diff --git a/src/Mod/Draft/draftmake/make_label.py b/src/Mod/Draft/draftmake/make_label.py index 6a92c203e9..c4505b8178 100644 --- a/src/Mod/Draft/draftmake/make_label.py +++ b/src/Mod/Draft/draftmake/make_label.py @@ -119,8 +119,10 @@ def make_label(target_point=App.Vector(0, 0, 0), - `'Area'` will show the `Area` of the target object's `Shape`, or of the indicated `'FaceN'` in `target`. - custom_text: str, optional + custom_text: str, or list of str, optional It defaults to `'Label'`. + If it is a list, each element in the list represents a new text line. + It is the text that will be displayed by the label when `label_type` is `'Custom'`. @@ -280,9 +282,14 @@ def make_label(target_point=App.Vector(0, 0, 0), if not custom_text: custom_text = "Label" try: - utils.type_check([(custom_text, str)], name=_name) + utils.type_check([(custom_text, (str, list))], name=_name) except TypeError: - _err(translate("draft","Wrong input: must be a string.")) + _err(translate("draft","Wrong input: must be a list of strings or a single string.")) + return None + + if (type(custom_text) is list + and not all(isinstance(element, str) for element in custom_text)): + _err(translate("draft","Wrong input: must be a list of strings or a single string.")) return None _msg("direction: {}".format(direction)) diff --git a/src/Mod/Material/StandardMaterial/Graphite.FCMat b/src/Mod/Material/StandardMaterial/Graphite.FCMat index 64477cbcf6..7c2cd08756 100644 --- a/src/Mod/Material/StandardMaterial/Graphite.FCMat +++ b/src/Mod/Material/StandardMaterial/Graphite.FCMat @@ -1,7 +1,7 @@ ; Graphite -; Uwe Stöhr, LGPL +; Uwe Stöhr, LGPL ; information about the content of such cards can be found on the wiki: -; https://www.freecadweb.org/wiki/Material +; https://www.freecadweb.org/wiki/Material ; file created by FreeCAD 0.20.24516 (Git) [General] diff --git a/src/Mod/Path/Gui/Resources/panels/PageOpDrillingEdit.ui b/src/Mod/Path/Gui/Resources/panels/PageOpDrillingEdit.ui index d7cd354325..daf7d1f3c1 100644 --- a/src/Mod/Path/Gui/Resources/panels/PageOpDrillingEdit.ui +++ b/src/Mod/Path/Gui/Resources/panels/PageOpDrillingEdit.ui @@ -73,33 +73,6 @@ - - - - false - - - Depth - - - - - - - false - - - Retract - - - - - - - false - - - @@ -107,34 +80,6 @@ - - - - Dwell - - - - - - - false - - - - - - - false - - - - - - - Extend Depth - - - @@ -154,34 +99,58 @@ - - - - - Off - - - - - A(x) - - - - - B(y) - - - - - A & B - - + + + + false + + + Retract + - - + + + + false + + + + + + + false + + + + + + + false + + + + + - Enable Rotation + Dwell + + + + + + + false + + + Depth + + + + + + + Extend Depth diff --git a/src/Mod/Path/Gui/Resources/panels/PageOpPocketFullEdit.ui b/src/Mod/Path/Gui/Resources/panels/PageOpPocketFullEdit.ui index a7630a0a94..15708312cc 100644 --- a/src/Mod/Path/Gui/Resources/panels/PageOpPocketFullEdit.ui +++ b/src/Mod/Path/Gui/Resources/panels/PageOpPocketFullEdit.ui @@ -230,37 +230,6 @@ - - - - Enable Rotation - - - - - - - - Off - - - - - A(x) - - - - - B(y) - - - - - A & B - - - - diff --git a/src/Mod/Path/Gui/Resources/panels/PageOpProfileFullEdit.ui b/src/Mod/Path/Gui/Resources/panels/PageOpProfileFullEdit.ui index 7ccd70cfb7..707a72ed61 100644 --- a/src/Mod/Path/Gui/Resources/panels/PageOpProfileFullEdit.ui +++ b/src/Mod/Path/Gui/Resources/panels/PageOpProfileFullEdit.ui @@ -57,10 +57,10 @@ - - + + - Cut Side + Direction @@ -81,10 +81,16 @@ - - - - Direction + + + + + 0 + 0 + + + + <html><head/><body><p>The amount of extra material left by this operation in relation to the target shape.</p></body></html> @@ -105,6 +111,13 @@ + + + + Cut Side + + + @@ -112,50 +125,6 @@ - - - - - 0 - 0 - - - - <html><head/><body><p>The amount of extra material left by this operation in relation to the target shape.</p></body></html> - - - - - - - Enable Rotation - - - - - - - - Off - - - - - A(x) - - - - - B(y) - - - - - A & B - - - - diff --git a/src/Mod/Path/PathScripts/PathAreaOp.py b/src/Mod/Path/PathScripts/PathAreaOp.py index c0854934a3..483560e865 100644 --- a/src/Mod/Path/PathScripts/PathAreaOp.py +++ b/src/Mod/Path/PathScripts/PathAreaOp.py @@ -41,12 +41,11 @@ __title__ = "Base class for PathArea based operations." __author__ = "sliptonic (Brad Collette)" __url__ = "https://www.freecadweb.org" __doc__ = "Base class and properties for Path.Area based operations." +__contributors__ = "russ4262 (Russell Johnson)" -LOGLEVEL = PathLog.Level.INFO -PathLog.setLevel(LOGLEVEL, PathLog.thisModule()) -if LOGLEVEL is PathLog.Level.DEBUG: - PathLog.trackModule() +PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +# PathLog.trackModule(PathLog.thisModule()) # Qt translation handling @@ -66,7 +65,9 @@ class ObjectOp(PathOp.ObjectOp): '''opFeatures(obj) ... returns the base features supported by all Path.Area based operations. The standard feature list is OR'ed with the return value of areaOpFeatures(). Do not overwrite, implement areaOpFeatures(obj) instead.''' - return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureStepDown | PathOp.FeatureHeights | PathOp.FeatureStartPoint | self.areaOpFeatures(obj) | PathOp.FeatureCoolant + return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureStepDown \ + | PathOp.FeatureHeights | PathOp.FeatureStartPoint \ + | self.areaOpFeatures(obj) | PathOp.FeatureCoolant def areaOpFeatures(self, obj): '''areaOpFeatures(obj) ... overwrite to add operation specific features. @@ -79,9 +80,6 @@ class ObjectOp(PathOp.ObjectOp): Do not overwrite, overwrite initAreaOp(obj) instead.''' PathLog.track() - # These are static while document is open, if it contains a 3D Surface Op - self.initWithRotation = False - # Debugging obj.addProperty("App::PropertyString", "AreaParams", "Path") obj.setEditorMode('AreaParams', 2) # hide @@ -89,16 +87,9 @@ class ObjectOp(PathOp.ObjectOp): obj.setEditorMode('PathParams', 2) # hide obj.addProperty("Part::PropertyPartShape", "removalshape", "Path") obj.setEditorMode('removalshape', 2) # hide - # obj.Proxy = self - self.setupAdditionalProperties(obj) self.initAreaOp(obj) - def setupAdditionalProperties(self, obj): - if not hasattr(obj, 'EnableRotation'): - obj.addProperty("App::PropertyEnumeration", "EnableRotation", "Rotation", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable rotation to gain access to pockets/areas not normal to Z axis.")) - obj.EnableRotation = ['Off', 'A(x)', 'B(y)', 'A & B'] - def initAreaOp(self, obj): '''initAreaOp(obj) ... overwrite if the receiver class needs initialisation. Can safely be overwritten by subclasses.''' @@ -150,7 +141,6 @@ class ObjectOp(PathOp.ObjectOp): if hasattr(obj, prop): obj.setEditorMode(prop, 2) - self.setupAdditionalProperties(obj) self.areaOpOnDocumentRestored(obj) def areaOpOnDocumentRestored(self, obj): @@ -164,14 +154,6 @@ class ObjectOp(PathOp.ObjectOp): Do not overwrite, overwrite areaOpSetDefaultValues(obj, job) instead.''' PathLog.debug("opSetDefaultValues(%s, %s)" % (obj.Label, job.Label)) - # Initial setting for EnableRotation is taken from Job settings/SetupSheet - # User may override on per-operation basis as needed. - if hasattr(job.SetupSheet, 'SetupEnableRotation'): - obj.EnableRotation = job.SetupSheet.SetupEnableRotation - else: - obj.EnableRotation = 'Off' - PathLog.debug("opSetDefaultValues(): Enable Rotation: {}".format(obj.EnableRotation)) - if PathOp.FeatureDepths & self.opFeatures(obj): try: shape = self.areaOpShapeForDepths(obj, job) @@ -189,26 +171,8 @@ class ObjectOp(PathOp.ObjectOp): startDepth = bb.ZMax finalDepth = bb.ZMin - # Adjust start and final depths if rotation is enabled - if obj.EnableRotation != 'Off': - self.initWithRotation = True - self.stockBB = PathUtils.findParentJob(obj).Stock.Shape.BoundBox # pylint: disable=attribute-defined-outside-init - # Calculate rotational distances/radii - opHeights = self.opDetermineRotationRadii(obj) # return is list with tuples [(xRotRad, yRotRad, zRotRad), (clrOfst, safOfset)] - (xRotRad, yRotRad, zRotRad) = opHeights[0] # pylint: disable=unused-variable - PathLog.debug("opHeights[0]: " + str(opHeights[0])) - PathLog.debug("opHeights[1]: " + str(opHeights[1])) - - if obj.EnableRotation == 'A(x)': - startDepth = xRotRad - if obj.EnableRotation == 'B(y)': - startDepth = yRotRad - else: - startDepth = max(xRotRad, yRotRad) - finalDepth = -1 * startDepth - - obj.StartDepth.Value = startDepth - obj.FinalDepth.Value = finalDepth + # obj.StartDepth.Value = startDepth + # obj.FinalDepth.Value = finalDepth obj.OpStartDepth.Value = startDepth obj.OpFinalDepth.Value = finalDepth @@ -338,49 +302,9 @@ class ObjectOp(PathOp.ObjectOp): # Instantiate class variables for operation reference self.endVector = None # pylint: disable=attribute-defined-outside-init - self.rotateFlag = False # pylint: disable=attribute-defined-outside-init self.leadIn = 2.0 # pylint: disable=attribute-defined-outside-init - self.cloneNames = [] # pylint: disable=attribute-defined-outside-init - self.guiMsgs = [] # pylint: disable=attribute-defined-outside-init - self.tempObjectNames = [] # pylint: disable=attribute-defined-outside-init - self.stockBB = PathUtils.findParentJob(obj).Stock.Shape.BoundBox # pylint: disable=attribute-defined-outside-init - self.useTempJobClones('Delete') # Clear temporary group and recreate for temp job clones - self.rotStartDepth = None # pylint: disable=attribute-defined-outside-init - start_depth = obj.StartDepth.Value - final_depth = obj.FinalDepth.Value - if obj.EnableRotation != 'Off': - # Calculate operation heights based upon rotation radii - opHeights = self.opDetermineRotationRadii(obj) - (self.xRotRad, self.yRotRad, self.zRotRad) = opHeights[0] # pylint: disable=attribute-defined-outside-init - (self.clrOfset, self.safOfst) = opHeights[1] # pylint: disable=attribute-defined-outside-init - - # Set clearance and safe heights based upon rotation radii - if obj.EnableRotation == 'A(x)': - start_depth = self.xRotRad - elif obj.EnableRotation == 'B(y)': - start_depth = self.yRotRad - else: - start_depth = max(self.xRotRad, self.yRotRad) - final_depth = -1 * start_depth - - self.rotStartDepth = start_depth - # The next two lines are improper code. - # The ClearanceHeight and SafeHeight need to be set in opSetDefaultValues() method. - # They should not be redefined here, so this entire `if...:` statement needs relocated. - obj.ClearanceHeight.Value = start_depth + self.clrOfset - obj.SafeHeight.Value = start_depth + self.safOfst - - # Create visual axes when debugging. - if PathLog.getLevel(PathLog.thisModule()) == 4: - self.visualAxis() - - # Set axial feed rates based upon horizontal feed rates - safeCircum = 2 * math.pi * obj.SafeHeight.Value - self.axialFeed = 360 / safeCircum * self.horizFeed # pylint: disable=attribute-defined-outside-init - self.axialRapid = 360 / safeCircum * self.horizRapid # pylint: disable=attribute-defined-outside-init - - # Initiate depthparams and calculate operation heights for rotational operation + # Initiate depthparams and calculate operation heights for operation self.depthparams = self._customDepthParams(obj, obj.StartDepth.Value, obj.FinalDepth.Value) # Set start point @@ -391,13 +315,13 @@ class ObjectOp(PathOp.ObjectOp): aOS = self.areaOpShapes(obj) # pylint: disable=assignment-from-no-return - # Adjust tuples length received from other PathWB tools/operations beside PathPocketShape + # Adjust tuples length received from other PathWB tools/operations shapes = [] for shp in aOS: if len(shp) == 2: (fc, iH) = shp - # fc, iH, sub, angle, axis, strtDep, finDep - tup = fc, iH, 'otherOp', 0.0, 'S', start_depth, final_depth + # fc, iH, sub or description + tup = fc, iH, 'otherOp' shapes.append(tup) else: shapes.append(shp) @@ -420,23 +344,15 @@ class ObjectOp(PathOp.ObjectOp): shapes = [j['shape'] for j in jobs] sims = [] - numShapes = len(shapes) - for ns in range(0, numShapes): + for shape, isHole, sub in shapes: profileEdgesIsOpen = False - (shape, isHole, sub, angle, axis, strDep, finDep) = shapes[ns] # pylint: disable=unused-variable + if sub == 'OpenEdge': profileEdgesIsOpen = True if PathOp.FeatureStartPoint & self.opFeatures(obj) and obj.UseStartPoint: osp = obj.StartPoint self.commandlist.append(Path.Command('G0', {'X': osp.x, 'Y': osp.y, 'F': self.horizRapid})) - if ns < numShapes - 1: - nextAxis = shapes[ns + 1][4] - else: - nextAxis = 'L' - - self.depthparams = self._customDepthParams(obj, strDep, finDep) - try: if profileEdgesIsOpen: (pp, sim) = self._buildProfileOpenEdges(obj, shape, isHole, start, getsim) @@ -450,28 +366,6 @@ class ObjectOp(PathOp.ObjectOp): ppCmds = pp else: ppCmds = pp.Commands - if obj.EnableRotation != 'Off' and self.rotateFlag is True: - # Rotate model to index for cut - if axis == 'X': - axisOfRot = 'A' - elif axis == 'Y': - axisOfRot = 'B' - elif axis == 'Z': - axisOfRot = 'C' - else: - axisOfRot = 'A' - # Rotate Model to correct angle - ppCmds.insert(0, Path.Command('G0', {axisOfRot: angle, 'F': self.axialRapid})) - - # Raise cutter to safe height - ppCmds.insert(0, Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) - - # Return index to starting position if axis of rotation changes. - if numShapes > 1: - if ns != numShapes - 1: - 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) @@ -482,51 +376,6 @@ class ObjectOp(PathOp.ObjectOp): self.endVector[2] = obj.ClearanceHeight.Value self.commandlist.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) - # Raise cutter to safe height and rotate back to original orientation - # based on next rotational operation in job - if self.rotateFlag is True: - resetAxis = False - lastJobOp = None - nextJobOp = None - opIdx = 0 - JOB = PathUtils.findParentJob(obj) - jobOps = JOB.Operations.Group - numJobOps = len(jobOps) - - for joi in range(0, numJobOps): - jo = jobOps[joi] - if jo.Name == obj.Name: - opIdx = joi - lastOpIdx = opIdx - 1 - nextOpIdx = opIdx + 1 - if lastOpIdx > -1: - lastJobOp = jobOps[lastOpIdx] - if nextOpIdx < numJobOps: - nextJobOp = jobOps[nextOpIdx] - - if lastJobOp is not None: - if hasattr(lastJobOp, 'EnableRotation'): - PathLog.debug('Last Op, {}, has `EnableRotation` set to {}'.format(lastJobOp.Label, lastJobOp.EnableRotation)) - if lastJobOp.EnableRotation != obj.EnableRotation: - resetAxis = True - # if ns == numShapes - 1: # If last shape, check next op EnableRotation setting - if nextJobOp is not None: - if hasattr(nextJobOp, 'EnableRotation'): - PathLog.debug('Next Op, {}, has `EnableRotation` set to {}'.format(nextJobOp.Label, nextJobOp.EnableRotation)) - if nextJobOp.EnableRotation != obj.EnableRotation: - resetAxis = True - - # Raise to safe height if rotation activated - self.commandlist.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) - # reset rotational axes if necessary - if resetAxis is True: - 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 - for ton in self.tempObjectNames: # remove temporary objects by name - FreeCAD.ActiveDocument.removeObject(ton) PathLog.debug("obj.Name: " + str(obj.Name) + "\n\n") return sims @@ -561,461 +410,7 @@ class ObjectOp(PathOp.ObjectOp): # pylint: disable=unused-argument return False - # Rotation-related methods - def opDetermineRotationRadii(self, obj): - '''opDetermineRotationRadii(obj) - Determine rotational radii for 4th-axis rotations, for clearance/safe heights ''' - - parentJob = PathUtils.findParentJob(obj) - xlim = 0.0 - ylim = 0.0 - - # Determine boundbox radius based upon xzy limits data - if math.fabs(self.stockBB.ZMin) > math.fabs(self.stockBB.ZMax): - zlim = self.stockBB.ZMin - else: - zlim = self.stockBB.ZMax - - if obj.EnableRotation != 'B(y)': - # Rotation is around X-axis, cutter moves along same axis - if math.fabs(self.stockBB.YMin) > math.fabs(self.stockBB.YMax): - ylim = self.stockBB.YMin - else: - ylim = self.stockBB.YMax - - if obj.EnableRotation != 'A(x)': - # Rotation is around Y-axis, cutter moves along same axis - if math.fabs(self.stockBB.XMin) > math.fabs(self.stockBB.XMax): - xlim = self.stockBB.XMin - else: - xlim = self.stockBB.XMax - - xRotRad = math.sqrt(ylim**2 + zlim**2) - yRotRad = math.sqrt(xlim**2 + zlim**2) - zRotRad = math.sqrt(xlim**2 + ylim**2) - - clrOfst = parentJob.SetupSheet.ClearanceHeightOffset.Value - safOfst = parentJob.SetupSheet.SafeHeightOffset.Value - - return [(xRotRad, yRotRad, zRotRad), (clrOfst, safOfst)] - - def 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()" - rtn = True - orientation = 'X' - angle = 500.0 - precision = 6 - - 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 PathGeom.isRoughly(0.0, val) is True: - return 0.0 - # Convert VAL.99999999 to next integer - elif math.fabs(val % 1) > 1.0 - PathGeom.Tolerance: - return round(val) - else: - return round(val, precision) - - 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(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 - if saX == 0.0: - if saY == 0.0: - orientation = "Z" - if saZ == 1.0: - angle = 0.0 - elif saZ == -1.0: - angle = -180.0 - else: - praInfo += "_else_X" + str(saZ) - elif saY == 1.0: - orientation = "Y" - angle = 90.0 - elif saY == -1.0: - orientation = "Y" - angle = -90.0 - else: - if saZ != 0.0: - angle = math.degrees(math.atan(saY / saZ)) - orientation = "Y" - elif saY == 0.0: - if saZ == 0.0: - orientation = "X" - if saX == 1.0: - angle = -90.0 - elif saX == -1.0: - angle = 90.0 - else: - praInfo += "_else_X" + str(saX) - else: - orientation = "X" - ratio = saX / saZ - angle = math.degrees(math.atan(ratio)) - if ratio < 0.0: - praInfo += " NEG-ratio" - # angle -= 90 - else: - praInfo += " POS-ratio" - angle = -1 * angle - if saX < 0.0: - angle = angle + 180.0 - elif saZ == 0.0: - # if saY != 0.0: - angle = math.degrees(math.atan(saX / saY)) - orientation = "Y" - - if saX + nX == 0.0: - angle = -1 * angle - if saY + nY == 0.0: - angle = -1 * angle - if saZ + nZ == 0.0: - angle = -1 * angle - - if saY == -1.0 or saY == 1.0: - if nX != 0.0: - angle = -1 * angle - - # Enforce enabled rotation in settings - praInfo += "\n -Initial orientation: {}".format(orientation) - if orientation == 'Y': - axis = 'X' - if obj.EnableRotation == 'B(y)': # Required axis disabled - if angle == 180.0 or angle == -180.0: - axis = 'Y' - else: - rtn = False - elif orientation == 'X': - axis = 'Y' - if obj.EnableRotation == 'A(x)': # Required axis disabled - if angle == 180.0 or angle == -180.0: - axis = 'X' - else: - rtn = False - elif orientation == 'Z': - axis = 'X' - - if math.fabs(angle) == 0.0: - angle = 0.0 - rtn = False - - if angle == 500.0: - angle = 0.0 - rtn = False - - if rtn is False: - if orientation == 'Z' and angle == 0.0 and obj.ReverseDirection is True: - if obj.EnableRotation == 'B(y)': - axis = 'Y' - rtn = True - - if rtn is True: - self.rotateFlag = True # pylint: disable=attribute-defined-outside-init - if obj.ReverseDirection is True: - if angle < 180.0: - angle = angle + 180.0 - else: - angle = angle - 180.0 - 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" - - 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: - if len(self.guiMsgs) > 0: - if FreeCAD.GuiUp: - from PySide.QtGui import QMessageBox - for entry in self.guiMsgs: - (title, msg) = entry - QMessageBox.warning(None, title, msg) - self.guiMsgs = [] # pylint: disable=attribute-defined-outside-init - return True - else: - for entry in self.guiMsgs: - (title, msg) = entry - PathLog.warning("{}:: {}".format(title, msg)) - self.guiMsgs = [] # pylint: disable=attribute-defined-outside-init - return True - 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' - # zAx = 'zAxCyl' - VA = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", "visualAxis") - if FreeCAD.GuiUp: - FreeCADGui.ActiveDocument.getObject('visualAxis').Visibility = False - vaGrp = FreeCAD.ActiveDocument.getObject("visualAxis") - - FreeCAD.ActiveDocument.addObject("Part::Cylinder", xAx) - cyl = FreeCAD.ActiveDocument.getObject(xAx) - cyl.Label = xAx - cyl.Radius = self.xRotRad - cyl.Height = 0.01 - cyl.Placement = FreeCAD.Placement(FreeCAD.Vector(0, 0, 0), FreeCAD.Rotation(FreeCAD.Vector(0, 1, 0), 90)) - cyl.purgeTouched() - if FreeCAD.GuiUp: - cylGui = FreeCADGui.ActiveDocument.getObject(xAx) - cylGui.ShapeColor = (0.667, 0.000, 0.000) - cylGui.Transparency = 85 - cylGui.Visibility = False - vaGrp.addObject(cyl) - - FreeCAD.ActiveDocument.addObject("Part::Cylinder", yAx) - cyl = FreeCAD.ActiveDocument.getObject(yAx) - cyl.Label = yAx - cyl.Radius = self.yRotRad - cyl.Height = 0.01 - cyl.Placement = FreeCAD.Placement(FreeCAD.Vector(0, 0, 0), FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), 90)) - cyl.purgeTouched() - if FreeCAD.GuiUp: - cylGui = FreeCADGui.ActiveDocument.getObject(yAx) - cylGui.ShapeColor = (0.000, 0.667, 0.000) - cylGui.Transparency = 85 - cylGui.Visibility = False - vaGrp.addObject(cyl) - VA.purgeTouched() - - 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 == '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') - else: - FreeCAD.ActiveDocument.getObject('rotJobClones').purgeTouched() - else: - FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", "rotJobClones") - if FreeCAD.GuiUp: - FreeCADGui.ActiveDocument.getObject('rotJobClones').Visibility = False - - if cloneName != 'Start' and cloneName != 'Delete': - FreeCAD.ActiveDocument.getObject('rotJobClones').addObject(FreeCAD.ActiveDocument.getObject(cloneName)) - if FreeCAD.GuiUp: - 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. - tag = axis + '_' + axis + '_' + str(math.fabs(rndAng)).replace('.', '_') - else: - tag = axis + str(rndAng).replace('.', '_') - clnNm = obj.Name + '_base_' + '_' + str(subCount) + '_' + tag - stckClnNm = obj.Name + '_stock_' + '_' + str(subCount) + '_' + tag - if clnNm not in self.cloneNames: - self.cloneNames.append(clnNm) - self.cloneNames.append(stckClnNm) - if FreeCAD.ActiveDocument.getObject(clnNm): - FreeCAD.ActiveDocument.getObject(clnNm).Shape = base.Shape - else: - FreeCAD.ActiveDocument.addObject('Part::Feature', clnNm).Shape = base.Shape - self.useTempJobClones(clnNm) - if FreeCAD.ActiveDocument.getObject(stckClnNm): - FreeCAD.ActiveDocument.getObject(stckClnNm).Shape = PathUtils.findParentJob(obj).Stock.Shape - else: - FreeCAD.ActiveDocument.addObject('Part::Feature', stckClnNm).Shape = PathUtils.findParentJob(obj).Stock.Shape - self.useTempJobClones(stckClnNm) - if FreeCAD.GuiUp: - FreeCADGui.ActiveDocument.getObject(stckClnNm).Transparency = 90 - FreeCADGui.ActiveDocument.getObject(clnNm).ShapeColor = (1.000, 0.667, 0.000) - clnBase = FreeCAD.ActiveDocument.getObject(clnNm) - clnStock = FreeCAD.ActiveDocument.getObject(stckClnNm) - tag = base.Name + '_' + tag - return (clnBase, clnStock, tag) - - def 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) - surf = FreeCAD.Vector(0.0, 0.0, 0.0) - - if hasattr(face, 'normalAt'): - n = face.normalAt(0, 0) - elif hasattr(face, 'normal'): - n = face.normal(0, 0) - if hasattr(face.Surface, 'Axis'): - s = face.Surface.Axis - else: - s = n - norm.x = n.x - norm.y = n.y - norm.z = n.z - surf.x = s.x - surf.y = s.y - surf.z = s.z - return (norm, surf) - - def 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 - ''' - if axis == 'X': - vect = FreeCAD.Vector(1, 0, 0) - elif axis == 'Y': - vect = FreeCAD.Vector(0, 1, 0) - - # Commented out to fix PocketShape InverseAngle rotation problem - # if obj.InverseAngle is True: - # angle = -1 * angle - # if math.fabs(angle) == 0.0: - # angle = 0.0 - - # Create a temporary clone of model for rotational use. - (clnBase, clnStock, tag) = self.cloneBaseAndStock(obj, base, angle, axis, subCount) - - # Rotate base to such that Surface.Axis of pocket bottom is Z=1 - clnBase = Draft.rotate(clnBase, angle, center=FreeCAD.Vector(0.0, 0.0, 0.0), axis=vect, copy=False) - clnStock = Draft.rotate(clnStock, angle, center=FreeCAD.Vector(0.0, 0.0, 0.0), axis=vect, copy=False) - - clnBase.purgeTouched() - clnStock.purgeTouched() - return (clnBase, angle, clnStock, tag) - - 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': - vect = FreeCAD.Vector(0, 1, 0) - # Rotate base to inverse of original angle - clnBase = Draft.rotate(clnBase, (-2 * angle), center=FreeCAD.Vector(0.0, 0.0, 0.0), axis=vect, copy=False) - clnStock = Draft.rotate(clnStock, (-2 * angle), center=FreeCAD.Vector(0.0, 0.0, 0.0), axis=vect, copy=False) - clnBase.purgeTouched() - clnStock.purgeTouched() - # Update property and angle values - obj.InverseAngle = True - # obj.AttemptInverseAngle = False - angle = -1 * angle - - PathLog.debug(translate("Path", "Rotated to inverse angle.")) - return (clnBase, clnStock, angle) - - def sortTuplesByIndex(self, TupleList, tagIdx): - '''sortTuplesByIndex(TupleList, tagIdx) - sort list of tuples based on tag index provided - return (TagList, GroupList) - ''' - # Separate elements, regroup by orientation (axis_angle combination) - TagList = ['X34.2'] - GroupList = [[(2.3, 3.4, 'X')]] - for tup in TupleList: - if tup[tagIdx] in TagList: - # Determine index of found string - i = 0 - for orn in TagList: - if orn == tup[4]: - break - i += 1 - GroupList[i].append(tup) - else: - TagList.append(tup[4]) # add orientation entry - GroupList.append([tup]) # add orientation entry - # Remove temp elements - TagList.pop(0) - GroupList.pop(0) - return (TagList, GroupList) - - def warnDisabledAxis(self, obj, axis, sub=''): - '''warnDisabledAxis(self, obj, axis) - Provide user feedback if required axis is disabled''' - if axis == 'X' and obj.EnableRotation == 'B(y)': - msg = translate('Path', "{}:: {} is inaccessible.".format(obj.Name, sub)) + " " - msg += translate('Path', "Selected feature(s) require 'Enable Rotation: A(x)' for access.") - PathLog.warning(msg) - return True - elif axis == 'Y' and obj.EnableRotation == 'A(x)': - msg = translate('Path', "{}:: {} is inaccessible.".format(obj.Name, sub)) + " " - msg += translate('Path', "Selected feature(s) require 'Enable Rotation: B(y)' for access.") - PathLog.warning(msg) - return True - else: - return False - - def isFaceUp(self, base, face): - '''isFaceUp(base, face) ... - When passed a base object and face shape, returns True if face is up. - This method is used to identify correct rotation of a model. - ''' - # verify face is normal to Z+- - (norm, surf) = self.getFaceNormAndSurf(face) - if round(abs(norm.z), 8) != 1.0 or round(abs(surf.z), 8) != 1.0: - PathLog.debug('isFaceUp - face not oriented normal to Z+-') - return False - - up = face.extrude(FreeCAD.Vector(0.0, 0.0, 5.0)) - dwn = face.extrude(FreeCAD.Vector(0.0, 0.0, -5.0)) - upCmn = base.Shape.common(up) - dwnCmn = base.Shape.common(dwn) - - # Identify orientation based on volumes of common() results - if len(upCmn.Edges) > 0: - PathLog.debug('isFaceUp - HAS up edges\n') - if len(dwnCmn.Edges) > 0: - PathLog.debug('isFaceUp - up and dwn edges\n') - dVol = round(dwnCmn.Volume, 6) - uVol = round(upCmn.Volume, 6) - if uVol > dVol: - return False - return True - else: - if round(upCmn.Volume, 6) == 0.0: - return True - return False - elif len(dwnCmn.Edges) > 0: - PathLog.debug('isFaceUp - HAS dwn edges only\n') - dVol = round(dwnCmn.Volume, 6) - if dVol == 0.0: - return False - return True - PathLog.debug('isFaceUp - exit True\n') - return True - + # Support methods def _customDepthParams(self, obj, strDep, finDep): finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0 cdp = PathUtils.depth_params( @@ -1030,5 +425,5 @@ class ObjectOp(PathOp.ObjectOp): # Eclass def SetupProperties(): - setup = ['EnableRotation'] + setup = [] return setup diff --git a/src/Mod/Path/PathScripts/PathCircularHoleBase.py b/src/Mod/Path/PathScripts/PathCircularHoleBase.py index c12d6c1075..c6739738c7 100644 --- a/src/Mod/Path/PathScripts/PathCircularHoleBase.py +++ b/src/Mod/Path/PathScripts/PathCircularHoleBase.py @@ -26,7 +26,6 @@ import PathScripts.PathOp as PathOp import PathScripts.PathUtils as PathUtils from PySide import QtCore -import PathScripts.PathGeom as PathGeom # lazily loaded modules from lazy_loader.lazy_loader import LazyLoader @@ -35,15 +34,11 @@ Draft = LazyLoader('Draft', globals(), 'Draft') Part = LazyLoader('Part', globals(), 'Part') DraftGeomUtils = LazyLoader('DraftGeomUtils', globals(), 'DraftGeomUtils') -import math -if FreeCAD.GuiUp: - import FreeCADGui __title__ = "Path Circular Holes Base Operation" __author__ = "sliptonic (Brad Collette)" __url__ = "https://www.freecadweb.org" __doc__ = "Base class an implementation for operations on circular holes." -__contributors__ = "russ4262 (Russell Johnson)" # Qt translation handling @@ -57,39 +52,32 @@ PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) class ObjectOp(PathOp.ObjectOp): '''Base class for proxy objects of all operations on circular holes.''' - # These are static while document is open, if it contains a CircularHole Op - initOpFinalDepth = None - initOpStartDepth = None - initWithRotation = False - defValsSet = False - docRestored = False def opFeatures(self, obj): '''opFeatures(obj) ... calls circularHoleFeatures(obj) and ORs in the standard features required for processing circular holes. Do not overwrite, implement circularHoleFeatures(obj) instead''' - return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureHeights | PathOp.FeatureBaseFaces | self.circularHoleFeatures(obj) | PathOp.FeatureCoolant + return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureHeights \ + | PathOp.FeatureBaseFaces | self.circularHoleFeatures(obj) \ + | PathOp.FeatureCoolant def circularHoleFeatures(self, obj): '''circularHoleFeatures(obj) ... overwrite to add operations specific features. Can safely be overwritten by subclasses.''' - # pylint: disable=unused-argument return 0 def initOperation(self, obj): '''initOperation(obj) ... adds Disabled properties and calls initCircularHoleOperation(obj). Do not overwrite, implement initCircularHoleOperation(obj) instead.''' obj.addProperty("App::PropertyStringList", "Disabled", "Base", QtCore.QT_TRANSLATE_NOOP("Path", "List of disabled features")) - self.initCircularHoleOperation(obj) def initCircularHoleOperation(self, obj): '''initCircularHoleOperation(obj) ... overwrite if the subclass needs initialisation. Can safely be overwritten by subclasses.''' - pass # pylint: disable=unnecessary-pass + pass def baseIsArchPanel(self, obj, base): '''baseIsArchPanel(obj, base) ... return true if op deals with an Arch.Panel.''' - # pylint: disable=unused-argument return hasattr(base, "Proxy") and isinstance(base.Proxy, ArchPanel.PanelSheet) def getArchPanelEdge(self, obj, base, sub): @@ -101,7 +89,6 @@ class ObjectOp(PathOp.ObjectOp): Obviously this is as fragile as can be, but currently the best we can do while the panel sheets hide the actual features from Path and they can't be referenced directly. ''' - # pylint: disable=unused-argument ids = sub.split(".") holeId = int(ids[0]) wireId = int(ids[1]) @@ -136,7 +123,8 @@ class ObjectOp(PathOp.ObjectOp): shape.Edges[i].Curve.Radius * 2 > shape.BoundBox.XLength*0.9): return shape.Edges[i].Curve.Radius * 2 - # for all other shapes the diameter is just the dimension in X. This may be inaccurate as the BoundBox is calculated on the tessellated geometry + # for all other shapes the diameter is just the dimension in X. + # This may be inaccurate as the BoundBox is calculated on the tessellated geometry PathLog.warning(translate("Path", "Hole diameter may be inaccurate due to tessellation on face. Consider selecting hole edge.")) return shape.BoundBox.XLength except Part.OCCError as e: @@ -185,143 +173,34 @@ class ObjectOp(PathOp.ObjectOp): Do not overwrite, implement circularHoleExecute(obj, holes) instead.''' PathLog.track() - holes = [] - baseSubsTuples = [] - subCount = 0 - allTuples = [] - self.cloneNames = [] # pylint: disable=attribute-defined-outside-init - self.guiMsgs = [] # pylint: disable=attribute-defined-outside-init - self.rotateFlag = False # pylint: disable=attribute-defined-outside-init - self.useTempJobClones('Delete') # pylint: disable=attribute-defined-outside-init - self.stockBB = PathUtils.findParentJob(obj).Stock.Shape.BoundBox # pylint: disable=attribute-defined-outside-init - self.clearHeight = obj.ClearanceHeight.Value # pylint: disable=attribute-defined-outside-init - self.safeHeight = obj.SafeHeight.Value # pylint: disable=attribute-defined-outside-init - self.axialFeed = 0.0 # pylint: disable=attribute-defined-outside-init - self.axialRapid = 0.0 # pylint: disable=attribute-defined-outside-init - def haveLocations(self, obj): if PathOp.FeatureLocations & self.opFeatures(obj): return len(obj.Locations) != 0 return False - if obj.EnableRotation == 'Off': - strDep = obj.StartDepth.Value - finDep = obj.FinalDepth.Value - else: - # Calculate operation heights based upon rotation radii - opHeights = self.opDetermineRotationRadii(obj) - (self.xRotRad, self.yRotRad, self.zRotRad) = opHeights[0] # pylint: disable=attribute-defined-outside-init - (clrOfset, safOfst) = opHeights[1] - PathLog.debug("Exec. opHeights[0]: " + str(opHeights[0])) - PathLog.debug("Exec. opHeights[1]: " + str(opHeights[1])) - - # Set clearance and safe heights based upon rotation radii - if obj.EnableRotation == 'A(x)': - strDep = self.xRotRad - elif obj.EnableRotation == 'B(y)': - strDep = self.yRotRad - else: - strDep = max(self.xRotRad, self.yRotRad) - finDep = -1 * strDep - - obj.ClearanceHeight.Value = strDep + clrOfset - obj.SafeHeight.Value = strDep + safOfst - - # Create visual axes when debugging. - if PathLog.getLevel(PathLog.thisModule()) == 4: - self.visualAxis() - - # Set axial feed rates based upon horizontal feed rates - safeCircum = 2 * math.pi * obj.SafeHeight.Value - self.axialFeed = 360 / safeCircum * self.horizFeed # pylint: disable=attribute-defined-outside-init - self.axialRapid = 360 / safeCircum * self.horizRapid # pylint: disable=attribute-defined-outside-init - - # Complete rotational analysis and temp clone creation as needed - if obj.EnableRotation == 'Off': - PathLog.debug("Enable Rotation setting is 'Off' for {}.".format(obj.Name)) - stock = PathUtils.findParentJob(obj).Stock - for (base, subList) in obj.Base: - baseSubsTuples.append((base, subList, 0.0, 'A', stock)) - else: - for p in range(0, len(obj.Base)): - (bst, at) = self.process_base_geometry_with_rotation(obj, p, subCount) - allTuples.extend(at) - baseSubsTuples.extend(bst) - - for base, subs, angle, axis, stock in baseSubsTuples: - # rotate shorter angle in opposite direction - if angle > 180: - angle -= 360 - elif angle < -180: - angle += 360 - - # Re-analyze rotated model for drillable holes - if obj.EnableRotation != 'Off': - rotated_features = self.findHoles(obj, base) + holes = [] + for base, subs in obj.Base: for sub in subs: - PathLog.debug('sub, angle, axis: {}, {}, {}'.format(sub, angle, axis)) + PathLog.debug('processing {} in {}'.format(sub, base.Name)) if self.isHoleEnabled(obj, base, sub): pos = self.holePosition(obj, base, sub) if pos: - # Identify face to which edge belongs - sub_shape = base.Shape.getElement(sub) + holes.append({'x': pos.x, 'y': pos.y, 'r': self.holeDiameter(obj, base, sub)}) - # Default is to treat selection as 'Face' shape - holeBtm = sub_shape.BoundBox.ZMin - - if obj.EnableRotation != 'Off': - # Update Start and Final depths due to rotation, if auto defaults are active - parent_face = self._find_parent_face_of_edge(rotated_features, sub_shape) - if parent_face: - PathLog.debug('parent_face found') - holeBtm = parent_face.BoundBox.ZMin - if obj.OpStartDepth == obj.StartDepth: - obj.StartDepth.Value = parent_face.BoundBox.ZMax - PathLog.debug('new StartDepth: {}'.format(obj.StartDepth.Value)) - if obj.OpFinalDepth == obj.FinalDepth: - obj.FinalDepth.Value = holeBtm - PathLog.debug('new FinalDepth: {}'.format(holeBtm)) - else: - PathLog.debug('NO parent_face identified') - - if base.Shape.getElement(sub).ShapeType == 'Edge': - msg = translate("Path", "Verify Final Depth of holes based on edges. {} depth is: {} mm".format(sub, round(holeBtm, 4))) + " " - msg += translate("Path", "Always select the bottom edge of the hole when using an edge.") - PathLog.warning(msg) - - # Warn user if Final Depth set lower than bottom of hole - if obj.FinalDepth.Value < holeBtm: - msg = translate("Path", "Final Depth setting is below the hole bottom for {}.".format(sub)) + ' ' - msg += translate("Path", "{} depth is calculated at {} mm".format(sub, round(holeBtm, 4))) - PathLog.warning(msg) - - holes.append({'x': pos.x, 'y': pos.y, 'r': self.holeDiameter(obj, base, sub), - 'angle': angle, 'axis': axis, 'trgtDep': obj.FinalDepth.Value, - 'stkTop': stock.Shape.BoundBox.ZMax}) - - # haveLocations are populated from user-provided (x, y) coordinates - # provided by the user in the Base Locations tab of the Task Editor window if haveLocations(self, obj): for location in obj.Locations: - # holes.append({'x': location.x, 'y': location.y, 'r': 0, 'angle': 0.0, 'axis': 'X', 'holeBtm': obj.FinalDepth.Value}) - holes.append({'x': location.x, 'y': location.y, 'r': 0, - 'angle': 0.0, 'axis': 'X', 'trgtDep': obj.FinalDepth.Value, - 'stkTop': PathUtils.findParentJob(obj).Stock.Shape.BoundBox.ZMax}) + holes.append({'x': location.x, 'y': location.y, 'r': 0}) if len(holes) > 0: - self.circularHoleExecute(obj, holes) # circularHoleExecute() located in PathDrilling.py - - 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)) + self.circularHoleExecute(obj, holes) def circularHoleExecute(self, obj, holes): '''circularHoleExecute(obj, holes) ... implement processing of holes. holes is a list of dictionaries with 'x', 'y' and 'r' specified for each hole. Note that for Vertexes, non-circular Edges and Locations r=0. Must be overwritten by subclasses.''' - pass # pylint: disable=unnecessary-pass + pass def findAllHoles(self, obj): '''findAllHoles(obj) ... find all holes of all base models and assign as features.''' @@ -390,615 +269,4 @@ class ObjectOp(PathOp.ObjectOp): PathLog.debug("Found hole feature %s.%s" % (baseobject.Label, candidateFaceName)) PathLog.debug("holes found: {}".format(holelist)) - return features - - # Rotation-related methods - def opDetermineRotationRadii(self, obj): - '''opDetermineRotationRadii(obj) - Determine rotational radii for 4th-axis rotations, for clearance/safe heights ''' - - parentJob = PathUtils.findParentJob(obj) - xlim = 0.0 - ylim = 0.0 - - # Determine boundbox radius based upon xzy limits data - if math.fabs(self.stockBB.ZMin) > math.fabs(self.stockBB.ZMax): - zlim = self.stockBB.ZMin - else: - zlim = self.stockBB.ZMax - - if obj.EnableRotation != 'B(y)': - # Rotation is around X-axis, cutter moves along same axis - if math.fabs(self.stockBB.YMin) > math.fabs(self.stockBB.YMax): - ylim = self.stockBB.YMin - else: - ylim = self.stockBB.YMax - - if obj.EnableRotation != 'A(x)': - # Rotation is around Y-axis, cutter moves along same axis - if math.fabs(self.stockBB.XMin) > math.fabs(self.stockBB.XMax): - xlim = self.stockBB.XMin - else: - xlim = self.stockBB.XMax - - xRotRad = math.sqrt(ylim**2 + zlim**2) - yRotRad = math.sqrt(xlim**2 + zlim**2) - zRotRad = math.sqrt(xlim**2 + ylim**2) - - clrOfst = parentJob.SetupSheet.ClearanceHeightOffset.Value - safOfst = parentJob.SetupSheet.SafeHeightOffset.Value - - return [(xRotRad, yRotRad, zRotRad), (clrOfst, safOfst)] - - def 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(): " - rtn = True - orientation = 'X' - angle = 500.0 - precision = 6 - - 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 PathGeom.isRoughly(0.0, val) is True: - return 0.0 - # Convert VAL.99999999 to next integer - elif math.fabs(val % 1) > 1.0 - PathGeom.Tolerance: - return round(val) - else: - return round(val, precision) - - 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(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 - if saX == 0.0: - if saY == 0.0: - orientation = "Z" - if saZ == 1.0: - angle = 0.0 - elif saZ == -1.0: - angle = -180.0 - else: - praInfo += "_else_X" + str(saZ) - elif saY == 1.0: - orientation = "Y" - angle = 90.0 - elif saY == -1.0: - orientation = "Y" - angle = -90.0 - else: - if saZ != 0.0: - angle = math.degrees(math.atan(saY / saZ)) - orientation = "Y" - elif saY == 0.0: - if saZ == 0.0: - orientation = "X" - if saX == 1.0: - angle = -90.0 - elif saX == -1.0: - angle = 90.0 - else: - praInfo += "_else_X" + str(saX) - else: - orientation = "X" - ratio = saX / saZ - angle = math.degrees(math.atan(ratio)) - if ratio < 0.0: - praInfo += " NEG-ratio" - # angle -= 90 - else: - praInfo += " POS-ratio" - angle = -1 * angle - if saX < 0.0: - angle = angle + 180.0 - elif saZ == 0.0: - # if saY != 0.0: - angle = math.degrees(math.atan(saX / saY)) - orientation = "Y" - - if saX + nX == 0.0: - angle = -1 * angle - if saY + nY == 0.0: - angle = -1 * angle - if saZ + nZ == 0.0: - angle = -1 * angle - - if saY == -1.0 or saY == 1.0: - if nX != 0.0: - angle = -1 * angle - - # Enforce enabled rotation in settings - praInfo += "\n -Initial orientation: {}".format(orientation) - if orientation == 'Y': - axis = 'X' - if obj.EnableRotation == 'B(y)': # Required axis disabled - if angle == 180.0 or angle == -180.0: - axis = 'Y' - else: - rtn = False - elif orientation == 'X': - axis = 'Y' - if obj.EnableRotation == 'A(x)': # Required axis disabled - if angle == 180.0 or angle == -180.0: - axis = 'X' - else: - rtn = False - elif orientation == 'Z': - axis = 'X' - - if math.fabs(angle) == 0.0: - angle = 0.0 - rtn = False - - if angle == 500.0: - angle = 0.0 - rtn = False - - if rtn is False: - if orientation == 'Z' and angle == 0.0 and obj.ReverseDirection is True: - if obj.EnableRotation == 'B(y)': - axis = 'Y' - rtn = True - - if rtn: - self.rotateFlag = True # pylint: disable=attribute-defined-outside-init - if obj.ReverseDirection is True: - if angle < 180.0: - angle = angle + 180.0 - else: - angle = angle - 180.0 - 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" - - 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: - if len(self.guiMsgs) > 0: - if FreeCAD.GuiUp: - from PySide.QtGui import QMessageBox - for entry in self.guiMsgs: - (title, msg) = entry - QMessageBox.warning(None, title, msg) - self.guiMsgs = [] # pylint: disable=attribute-defined-outside-init - return True - else: - for entry in self.guiMsgs: - (title, msg) = entry - PathLog.warning("{}:: {}".format(title, msg)) - self.guiMsgs = [] # pylint: disable=attribute-defined-outside-init - return True - return False - - def visualAxis(self): - '''visualAxis() - Create visual X & Y axis for use in orientation of rotational operations - Triggered only for PathLog.debug''' - fcad = FreeCAD.ActiveDocument - - if not fcad.getObject('xAxCyl'): - xAx = 'xAxCyl' - yAx = 'yAxCyl' - # zAx = 'zAxCyl' - visual_axis_obj = fcad.addObject("App::DocumentObjectGroup", "visualAxis") - if FreeCAD.GuiUp: - FreeCADGui.ActiveDocument.getObject('visualAxis').Visibility = False - vaGrp = fcad.getObject("visualAxis") - - fcad.addObject("Part::Cylinder", xAx) - cyl = fcad.getObject(xAx) - cyl.Label = xAx - cyl.Radius = self.xRotRad - cyl.Height = 0.01 - cyl.Placement = FreeCAD.Placement(FreeCAD.Vector(0, 0, 0), FreeCAD.Rotation(FreeCAD.Vector(0, 1, 0), 90)) - cyl.purgeTouched() - if FreeCAD.GuiUp: - cylGui = FreeCADGui.ActiveDocument.getObject(xAx) - cylGui.ShapeColor = (0.667, 0.000, 0.000) - cylGui.Transparency = 85 - cylGui.Visibility = False - vaGrp.addObject(cyl) - - fcad.addObject("Part::Cylinder", yAx) - cyl = fcad.getObject(yAx) - cyl.Label = yAx - cyl.Radius = self.yRotRad - cyl.Height = 0.01 - cyl.Placement = FreeCAD.Placement(FreeCAD.Vector(0, 0, 0), FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), 90)) - cyl.purgeTouched() - if FreeCAD.GuiUp: - cylGui = FreeCADGui.ActiveDocument.getObject(yAx) - cylGui.ShapeColor = (0.000, 0.667, 0.000) - cylGui.Transparency = 85 - cylGui.Visibility = False - vaGrp.addObject(cyl) - visual_axis_obj.purgeTouched() - - def useTempJobClones(self, cloneName): - '''useTempJobClones(cloneName) - Manage use of temporary model clones for rotational operation calculations. - Clones are stored in 'rotJobClones' group.''' - fcad = FreeCAD.ActiveDocument - - if fcad.getObject('rotJobClones'): - if cloneName == 'Start': - if PathLog.getLevel(PathLog.thisModule()) < 4: - for cln in fcad.getObject('rotJobClones').Group: - fcad.removeObject(cln.Name) - elif cloneName == 'Delete': - if PathLog.getLevel(PathLog.thisModule()) < 4: - for cln in fcad.getObject('rotJobClones').Group: - fcad.removeObject(cln.Name) - fcad.removeObject('rotJobClones') - else: - fcad.getObject('rotJobClones').purgeTouched() - else: - fcad.addObject("App::DocumentObjectGroup", "rotJobClones") - if FreeCAD.GuiUp: - FreeCADGui.ActiveDocument.getObject('rotJobClones').Visibility = False - - if cloneName != 'Start' and cloneName != 'Delete': - fcad.getObject('rotJobClones').addObject(fcad.getObject(cloneName)) - if FreeCAD.GuiUp: - 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. - fcad = FreeCAD.ActiveDocument - rndAng = round(angle, 8) - if rndAng < 0.0: # neg sign is converted to underscore in clone name creation. - tag = axis + '_' + axis + '_' + str(math.fabs(rndAng)).replace('.', '_') - else: - tag = axis + str(rndAng).replace('.', '_') - clnNm = obj.Name + '_base_' + '_' + str(subCount) + '_' + tag - stckClnNm = obj.Name + '_stock_' + '_' + str(subCount) + '_' + tag - if clnNm not in self.cloneNames: - self.cloneNames.append(clnNm) - self.cloneNames.append(stckClnNm) - if fcad.getObject(clnNm): - fcad.getObject(clnNm).Shape = base.Shape - else: - fcad.addObject('Part::Feature', clnNm).Shape = base.Shape - self.useTempJobClones(clnNm) - if fcad.getObject(stckClnNm): - fcad.getObject(stckClnNm).Shape = PathUtils.findParentJob(obj).Stock.Shape - else: - fcad.addObject('Part::Feature', stckClnNm).Shape = PathUtils.findParentJob(obj).Stock.Shape - self.useTempJobClones(stckClnNm) - if FreeCAD.GuiUp: - FreeCADGui.ActiveDocument.getObject(stckClnNm).Transparency = 90 - FreeCADGui.ActiveDocument.getObject(clnNm).ShapeColor = (1.000, 0.667, 0.000) - clnBase = fcad.getObject(clnNm) - clnStock = fcad.getObject(stckClnNm) - tag = base.Name + '_' + tag - return (clnBase, clnStock, tag) - - def 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) - surf = FreeCAD.Vector(0.0, 0.0, 0.0) - - if hasattr(face, 'normalAt'): - n = face.normalAt(0, 0) - elif hasattr(face, 'normal'): - n = face.normal(0, 0) - if hasattr(face.Surface, 'Axis'): - s = face.Surface.Axis - else: - s = n - norm.x = n.x - norm.y = n.y - norm.z = n.z - surf.x = s.x - surf.y = s.y - surf.z = s.z - return (norm, surf) - - def 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 - ''' - if axis == 'X': - vect = FreeCAD.Vector(1, 0, 0) - elif axis == 'Y': - vect = FreeCAD.Vector(0, 1, 0) - - # Create a temporary clone of model for rotational use. - (clnBase, clnStock, tag) = self.cloneBaseAndStock(obj, base, angle, axis, subCount) - - # Rotate base to such that Surface.Axis of pocket bottom is Z=1 - clnBase = Draft.rotate(clnBase, angle, center=FreeCAD.Vector(0.0, 0.0, 0.0), axis=vect, copy=False) - clnStock = Draft.rotate(clnStock, angle, center=FreeCAD.Vector(0.0, 0.0, 0.0), axis=vect, copy=False) - - clnBase.purgeTouched() - clnStock.purgeTouched() - return (clnBase, angle, clnStock, tag) - - 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': - vect = FreeCAD.Vector(0, 1, 0) - # Rotate base to inverse of original angle - clnBase = Draft.rotate(clnBase, (-2 * angle), center=FreeCAD.Vector(0.0, 0.0, 0.0), axis=vect, copy=False) - clnStock = Draft.rotate(clnStock, (-2 * angle), center=FreeCAD.Vector(0.0, 0.0, 0.0), axis=vect, copy=False) - clnBase.purgeTouched() - clnStock.purgeTouched() - # Update property and angle values - obj.InverseAngle = True - # obj.AttemptInverseAngle = False - angle = -1 * angle - - PathLog.debug(translate("Path", "Rotated to inverse angle.")) - return (clnBase, clnStock, angle) - - def calculateStartFinalDepths(self, obj, shape, stock): - '''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': - strDep = obj.StartDepth.Value - if strDep <= finDep: - strDep = stockTop - else: - strDep = min(obj.StartDepth.Value, stockTop) - if strDep <= finDep: - strDep = stockTop - msg = translate('Path', "Start depth <= face depth.\nIncreased to stock top.") - PathLog.error(msg) - return (strDep, finDep) - - def sortTuplesByIndex(self, TupleList, tagIdx): - '''sortTuplesByIndex(TupleList, tagIdx) - sort list of tuples based on tag index provided - return (TagList, GroupList) - ''' - # Separate elements, regroup by orientation (axis_angle combination) - TagList = ['X34.2'] - GroupList = [[(2.3, 3.4, 'X')]] - for tup in TupleList: - if tup[tagIdx] in TagList: - # Determine index of found string - i = 0 - for orn in TagList: - if orn == tup[4]: - break - i += 1 - GroupList[i].append(tup) - else: - TagList.append(tup[4]) # add orientation entry - GroupList.append([tup]) # add orientation entry - # Remove temp elements - TagList.pop(0) - GroupList.pop(0) - return (TagList, GroupList) - - def warnDisabledAxis(self, obj, axis, sub=''): - '''warnDisabledAxis(self, obj, axis) - Provide user feedback if required axis is disabled''' - if axis == 'X' and obj.EnableRotation == 'B(y)': - msg = translate('Path', "{}:: {} is inaccessible.".format(obj.Name, sub)) + " " - msg += translate('Path', "Selected feature(s) require 'Enable Rotation: A(x)' for access.") - PathLog.warning(msg) - return True - elif axis == 'Y' and obj.EnableRotation == 'A(x)': - msg = translate('Path', "{}:: {} is inaccessible.".format(obj.Name, sub)) + " " - msg += translate('Path', "Selected feature(s) require 'Enable Rotation: B(y)' for access.") - PathLog.warning(msg) - return True - else: - return False - - def isFaceUp(self, base, face): - '''isFaceUp(base, face) ... - When passed a base object and face shape, returns True if face is up. - This method is used to identify correct rotation of a model. - ''' - # verify face is normal to Z+- - (norm, surf) = self.getFaceNormAndSurf(face) - if round(abs(norm.z), 8) != 1.0 or round(abs(surf.z), 8) != 1.0: - PathLog.debug('isFaceUp - face not oriented normal to Z+-') - return False - - curve = face.OuterWire.Edges[0].Curve - if curve.TypeId == "Part::GeomCircle": - center = curve.Center - radius = curve.Radius * 1. - face = Part.Face(Part.Wire(Part.makeCircle(radius, center))) - - up = face.extrude(FreeCAD.Vector(0.0, 0.0, 5.0)) - dwn = face.extrude(FreeCAD.Vector(0.0, 0.0, -5.0)) - upCmn = base.Shape.common(up) - dwnCmn = base.Shape.common(dwn) - - # Identify orientation based on volumes of common() results - if len(upCmn.Edges) > 0: - PathLog.debug('isFaceUp - HAS up edges\n') - if len(dwnCmn.Edges) > 0: - PathLog.debug('isFaceUp - up and dwn edges\n') - dVol = round(dwnCmn.Volume, 6) - uVol = round(upCmn.Volume, 6) - if uVol > dVol: - return False - return True - else: - if round(upCmn.Volume, 6) == 0.0: - return True - return False - elif len(dwnCmn.Edges) > 0: - PathLog.debug('isFaceUp - HAS dwn edges only\n') - dVol = round(dwnCmn.Volume, 6) - if dVol == 0.0: - return False - return True - - PathLog.debug('isFaceUp - exit True') - return True - - def process_base_geometry_with_rotation(self, obj, p, subCount): - '''process_base_geometry_with_rotation(obj, p, subCount)... - This method is the control method for analyzing the selected features, - determining their rotational needs, and creating clones as needed - for rotational access for the pocketing operation. - - Requires the object, obj.Base index (p), and subCount reference arguments. - Returns two lists of tuples for continued processing into paths. - ''' - baseSubsTuples = [] - allTuples = [] - - (base, subsList) = obj.Base[p] - - PathLog.debug(translate('Path', "Processing subs individually ...")) - for sub in subsList: - subCount += 1 - tup = self.process_nonloop_sublist(obj, base, sub) - if tup: - allTuples.append(tup) - baseSubsTuples.append(tup) - - return (baseSubsTuples, allTuples) - - def process_nonloop_sublist(self, obj, base, sub): - '''process_nonloop_sublist(obj, sub)... - Process sublist with non-looped set of features when rotation is enabled. - ''' - - rtn = False - face = base.Shape.getElement(sub) - - if sub[:4] != 'Face': - if face.ShapeType == 'Edge': - edgToFace = Part.Face(Part.Wire(Part.__sortEdges__([face]))) - face = edgToFace - else: - ignoreSub = base.Name + '.' + sub - PathLog.error(translate('Path', "Selected feature is not a Face. Ignoring: {}".format(ignoreSub))) - return False - - (norm, surf) = self.getFaceNormAndSurf(face) - (rtn, angle, axis, praInfo) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable - PathLog.debug("initial rotational analysis: {}".format(praInfo)) - - clnBase = base - faceIA = clnBase.Shape.getElement(sub) - if faceIA.ShapeType == 'Edge': - edgToFace = Part.Face(Part.Wire(Part.__sortEdges__([faceIA]))) - faceIA = edgToFace - - if rtn is True: - faceNum = sub.replace('Face', '') - PathLog.debug("initial applyRotationalAnalysis") - (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) - if faceIA.ShapeType == 'Edge': - edgToFace = Part.Face(Part.Wire(Part.__sortEdges__([faceIA]))) - faceIA = edgToFace - - (norm, surf) = self.getFaceNormAndSurf(faceIA) - (rtn, praAngle, praAxis, praInfo2) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable - PathLog.debug("follow-up rotational analysis: {}".format(praInfo2)) - - isFaceUp = self.isFaceUp(clnBase, faceIA) - PathLog.debug('... initial isFaceUp: {}'.format(isFaceUp)) - - if isFaceUp: - rtn = False - PathLog.debug('returning analysis: {}, {}'.format(praAngle, praAxis)) - return (clnBase, [sub], angle, axis, clnStock) - - if round(abs(praAngle), 8) == 180.0: - rtn = False - if not isFaceUp: - PathLog.debug('initial isFaceUp is False') - angle = 0.0 - # Eif - - if rtn: - # initial rotation failed, attempt inverse rotation if user requests it - PathLog.debug(translate("Path", "Face appears misaligned after initial rotation.") + ' 2') - if obj.AttemptInverseAngle: - PathLog.debug(translate("Path", "Applying inverse angle automatically.")) - (clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle) - else: - if obj.InverseAngle: - PathLog.debug(translate("Path", "Applying inverse angle manually.")) - (clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle) - else: - msg = translate("Path", "Consider toggling the 'InverseAngle' property and recomputing.") - PathLog.warning(msg) - - faceIA = clnBase.Shape.getElement(sub) - if faceIA.ShapeType == 'Edge': - edgToFace = Part.Face(Part.Wire(Part.__sortEdges__([faceIA]))) - faceIA = edgToFace - if not self.isFaceUp(clnBase, faceIA): - angle += 180.0 - - # Normalize rotation angle - if angle < 0.0: - angle += 360.0 - elif angle > 360.0: - angle -= 360.0 - - return (clnBase, [sub], angle, axis, clnStock) - - if not self.warnDisabledAxis(obj, axis): - PathLog.debug(str(sub) + ": No rotation used") - axis = 'X' - angle = 0.0 - stock = PathUtils.findParentJob(obj).Stock - return (base, [sub], angle, axis, stock) - - def _find_parent_face_of_edge(self, rotated_features, test_shape): - '''_find_parent_face_of_edge(rotated_features, test_shape)... - Compare test_shape with each within rotated_features to identify - and return the parent face of the test_shape, if it exists.''' - for (base, sub) in rotated_features: - sub_shape = base.Shape.getElement(sub) - if test_shape.isSame(sub_shape): - return sub_shape - elif test_shape.isEqual(sub_shape): - return sub_shape - else: - for e in sub_shape.Edges: - if test_shape.isSame(e): - return sub_shape - elif test_shape.isEqual(e): - return sub_shape - return False + return features \ No newline at end of file diff --git a/src/Mod/Path/PathScripts/PathDrilling.py b/src/Mod/Path/PathScripts/PathDrilling.py index ff5a4abac6..907749a904 100644 --- a/src/Mod/Path/PathScripts/PathDrilling.py +++ b/src/Mod/Path/PathScripts/PathDrilling.py @@ -36,7 +36,7 @@ __title__ = "Path Drilling Operation" __author__ = "sliptonic (Brad Collette)" __url__ = "https://www.freecadweb.org" __doc__ = "Path Drilling operation." -__contributors__ = "russ4262 (Russell Johnson)" +__contributors__ = "IMBack!" PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) @@ -53,7 +53,6 @@ class ObjectDrilling(PathCircularHoleBase.ObjectOp): def circularHoleFeatures(self, obj): '''circularHoleFeatures(obj) ... drilling works on anything, turn on all Base geometries and Locations.''' - # return PathOp.FeatureBaseGeometry | PathOp.FeatureLocations | PathOp.FeatureRotation return PathOp.FeatureBaseGeometry | PathOp.FeatureLocations | PathOp.FeatureCoolant def initCircularHoleOperation(self, obj): @@ -64,30 +63,15 @@ class ObjectDrilling(PathCircularHoleBase.ObjectOp): obj.addProperty("App::PropertyBool", "DwellEnabled", "Drill", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable dwell")) obj.addProperty("App::PropertyBool", "AddTipLength", "Drill", QtCore.QT_TRANSLATE_NOOP("App::Property", "Calculate the tip length and subtract from final depth")) obj.addProperty("App::PropertyEnumeration", "ReturnLevel", "Drill", QtCore.QT_TRANSLATE_NOOP("App::Property", "Controls how tool retracts Default=G99")) - obj.ReturnLevel = ['G99', 'G98'] # Canned Cycle Return Level obj.addProperty("App::PropertyDistance", "RetractHeight", "Drill", QtCore.QT_TRANSLATE_NOOP("App::Property", "The height where feed starts and height during retract tool when path is finished while in a peck operation")) obj.addProperty("App::PropertyEnumeration", "ExtraOffset", "Drill", QtCore.QT_TRANSLATE_NOOP("App::Property", "How far the drill depth is extended")) - obj.ExtraOffset = ['None', 'Drill Tip', '2x Drill Tip'] # Canned Cycle Return Level - # Rotation related properties - if not hasattr(obj, 'EnableRotation'): - obj.addProperty("App::PropertyEnumeration", "EnableRotation", "Rotation", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable rotation to gain access to pockets/areas not normal to Z axis.")) - obj.EnableRotation = ['Off', 'A(x)', 'B(y)', 'A & B'] - if not hasattr(obj, 'ReverseDirection'): - obj.addProperty('App::PropertyBool', 'ReverseDirection', 'Rotation', QtCore.QT_TRANSLATE_NOOP('App::Property', 'Reverse direction of pocket operation.')) - if not hasattr(obj, 'InverseAngle'): - obj.addProperty('App::PropertyBool', 'InverseAngle', 'Rotation', QtCore.QT_TRANSLATE_NOOP('App::Property', 'Inverse the angle. Example: -22.5 -> 22.5 degrees.')) - if not hasattr(obj, 'AttemptInverseAngle'): - obj.addProperty('App::PropertyBool', 'AttemptInverseAngle', 'Rotation', QtCore.QT_TRANSLATE_NOOP('App::Property', 'Attempt the inverse angle for face access if original rotation fails.')) + obj.ReturnLevel = ['G99', 'G98'] # Canned Cycle Return Level + obj.ExtraOffset = ['None', 'Drill Tip', '2x Drill Tip'] # Canned Cycle Return Level def circularHoleExecute(self, obj, holes): '''circularHoleExecute(obj, holes) ... generate drill operation for each hole in holes.''' PathLog.track() - PathLog.debug("\ncircularHoleExecute() in PathDrilling.py") - - lastAxis = None - lastAngle = 0.0 - parentJob = PathUtils.findParentJob(obj) self.commandlist.append(Path.Command("(Begin Drilling)")) @@ -104,57 +88,30 @@ class ObjectDrilling(PathCircularHoleBase.ObjectOp): self.commandlist.append(Path.Command('G90')) self.commandlist.append(Path.Command(obj.ReturnLevel)) - for p in holes: - cmd = "G81" - cmdParams = {} - cmdParams['Z'] = p['trgtDep'] - tiplength - cmdParams['F'] = self.vertFeed - cmdParams['R'] = obj.RetractHeight.Value - if obj.PeckEnabled and obj.PeckDepth.Value > 0: - cmd = "G83" - cmdParams['Q'] = obj.PeckDepth.Value - elif obj.DwellEnabled and obj.DwellTime > 0: - cmd = "G82" - cmdParams['P'] = obj.DwellTime + cmd = "G81" + cmdParams = {} + cmdParams['Z'] = obj.FinalDepth.Value - tiplength + cmdParams['F'] = self.vertFeed + cmdParams['R'] = obj.RetractHeight.Value + if obj.PeckEnabled and obj.PeckDepth.Value > 0: + cmd = "G83" + cmdParams['Q'] = obj.PeckDepth.Value + elif obj.DwellEnabled and obj.DwellTime > 0: + cmd = "G82" + cmdParams['P'] = obj.DwellTime + + # parentJob = PathUtils.findParentJob(obj) + # startHeight = obj.StartDepth.Value + parentJob.SetupSheet.SafeHeightOffset.Value + startHeight = obj.StartDepth.Value + self.job.SetupSheet.SafeHeightOffset.Value + + for p in holes: params = {} params['X'] = p['x'] params['Y'] = p['y'] - if obj.EnableRotation != 'Off': - angle = p['angle'] - axis = p['axis'] - # Rotate model to index for hole - if axis == 'X': - axisOfRot = 'A' - elif axis == 'Y': - axisOfRot = 'B' - elif axis == 'Z': - axisOfRot = 'C' - else: - axisOfRot = 'A' - - # Set initial values for last axis and angle - if lastAxis is None: - lastAxis = axisOfRot - lastAngle = angle - - # Handle axial and angular transitions - if axisOfRot != lastAxis: - self.commandlist.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) - self.commandlist.append(Path.Command('G0', {lastAxis: 0.0, 'F': self.axialRapid})) - elif angle != lastAngle: - self.commandlist.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) - - # Prepare for drilling cycle - self.commandlist.append(Path.Command('G0', {axisOfRot: angle, 'F': self.axialRapid})) - - # Update retract height due to rotation - self.opSetDefaultRetractHeight(obj) - cmdParams['R'] = obj.RetractHeight.Value # move to hole location self.commandlist.append(Path.Command('G0', {'X': p['x'], 'Y': p['y'], 'F': self.horizRapid})) - startHeight = obj.StartDepth.Value + parentJob.SetupSheet.SafeHeightOffset.Value self.commandlist.append(Path.Command('G0', {'Z': startHeight, 'F': self.vertRapid})) self.commandlist.append(Path.Command('G1', {'Z': obj.StartDepth.Value, 'F': self.vertFeed})) @@ -168,36 +125,18 @@ class ObjectDrilling(PathCircularHoleBase.ObjectOp): self.commandlist.append(Path.Command('G80')) self.commandlist.append(Path.Command('G0', {'Z': obj.SafeHeight.Value})) - # shift axis and angle values - if obj.EnableRotation != 'Off': - lastAxis = axisOfRot - lastAngle = angle - - if obj.EnableRotation != 'Off': - self.commandlist.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) - self.commandlist.append(Path.Command('G0', {lastAxis: 0.0, 'F': self.axialRapid})) - - def opSetDefaultRetractHeight(self, obj, job=None): - '''opSetDefaultRetractHeight(obj, job) ... set default Retract Height value''' - - has_job = True - if not job: - job = PathUtils.findParentJob(obj) - has_job = False + def opSetDefaultValues(self, obj, job): + '''opSetDefaultValues(obj, job) ... set default value for RetractHeight''' + obj.ExtraOffset = "None" if hasattr(job.SetupSheet, 'RetractHeight'): obj.RetractHeight = job.SetupSheet.RetractHeight elif self.applyExpression(obj, 'RetractHeight', 'StartDepth+SetupSheet.SafeHeightOffset'): - if has_job: + if not job: obj.RetractHeight = 10 else: obj.RetractHeight.Value = obj.StartDepth.Value + 1.0 - def opSetDefaultValues(self, obj, job): - '''opSetDefaultValues(obj, job) ... Set default property values''' - - self.opSetDefaultRetractHeight(obj, job) - if hasattr(job.SetupSheet, 'PeckDepth'): obj.PeckDepth = job.SetupSheet.PeckDepth elif self.applyExpression(obj, 'PeckDepth', 'OpToolDiameter*0.75'): @@ -208,19 +147,6 @@ class ObjectDrilling(PathCircularHoleBase.ObjectOp): else: obj.DwellTime = 1 - obj.ReverseDirection = False - obj.InverseAngle = False - obj.AttemptInverseAngle = False - obj.ExtraOffset = "None" - - # Initial setting for EnableRotation is taken from Job SetupSheet - # User may override on per-operation basis as needed. - if hasattr(job.SetupSheet, 'SetupEnableRotation'): - obj.EnableRotation = job.SetupSheet.SetupEnableRotation - else: - obj.EnableRotation = 'Off' - - def SetupProperties(): setup = [] setup.append("PeckDepth") @@ -231,14 +157,9 @@ def SetupProperties(): setup.append("ReturnLevel") setup.append("ExtraOffset") setup.append("RetractHeight") - setup.append("EnableRotation") - setup.append("ReverseDirection") - setup.append("InverseAngle") - setup.append("AttemptInverseAngle") return setup - -def Create(name, obj=None): +def Create(name, obj = None): '''Create(name) ... Creates and returns a Drilling operation.''' if obj is None: obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) @@ -247,4 +168,4 @@ def Create(name, obj=None): if obj.Proxy: obj.Proxy.findAllHoles(obj) - return obj + return obj \ No newline at end of file diff --git a/src/Mod/Path/PathScripts/PathDrillingGui.py b/src/Mod/Path/PathScripts/PathDrillingGui.py index 4ebee90d5f..f18ae76e0c 100644 --- a/src/Mod/Path/PathScripts/PathDrillingGui.py +++ b/src/Mod/Path/PathScripts/PathDrillingGui.py @@ -98,8 +98,6 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage): obj.PeckEnabled = self.form.peckEnabled.isChecked() if obj.ExtraOffset != str(self.form.ExtraOffset.currentText()): obj.ExtraOffset = str(self.form.ExtraOffset.currentText()) - if obj.EnableRotation != str(self.form.enableRotation.currentText()): - obj.EnableRotation = str(self.form.enableRotation.currentText()) self.updateToolController(obj, self.form.toolController) self.updateCoolant(obj, self.form.coolantController) @@ -123,7 +121,6 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage): self.setupToolController(obj, self.form.toolController) self.setupCoolant(obj, self.form.coolantController) - self.selectInComboBox(obj.EnableRotation, self.form.enableRotation) def getSignalsForUpdate(self, obj): @@ -138,7 +135,6 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage): signals.append(self.form.toolController.currentIndexChanged) signals.append(self.form.coolantController.currentIndexChanged) signals.append(self.form.ExtraOffset.currentIndexChanged) - signals.append(self.form.enableRotation.currentIndexChanged) return signals diff --git a/src/Mod/Path/PathScripts/PathHelix.py b/src/Mod/Path/PathScripts/PathHelix.py index 683997ce2d..59a729ab84 100644 --- a/src/Mod/Path/PathScripts/PathHelix.py +++ b/src/Mod/Path/PathScripts/PathHelix.py @@ -64,11 +64,6 @@ class ObjectHelix(PathCircularHoleBase.ObjectOp): obj.addProperty("App::PropertyLength", "StepOver", "Helix Drill", translate("PathHelix", "Radius increment (must be smaller than tool diameter)")) obj.addProperty("App::PropertyLength", "StartRadius", "Helix Drill", translate("PathHelix", "Starting Radius")) - # Rotation related properties - if not hasattr(obj, 'EnableRotation'): - obj.addProperty("App::PropertyEnumeration", "EnableRotation", "Rotation", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable rotation to gain access to pockets/areas not normal to Z axis.")) - obj.EnableRotation = ['Off', 'A(x)', 'B(y)', 'A & B'] - def opOnDocumentRestored(self, obj): if not hasattr(obj, 'StartRadius'): obj.addProperty("App::PropertyLength", "StartRadius", "Helix Drill", translate("PathHelix", "Starting Radius")) @@ -209,21 +204,12 @@ class ObjectHelix(PathCircularHoleBase.ObjectOp): obj.StartSide = "Inside" obj.StepOver = 100 - # Initial setting for EnableRotation is taken from Job SetupSheet - # User may override on per-operation basis as needed. - parentJob = findParentJob(obj) # PathUtils.findParentJob(obj) - if hasattr(parentJob.SetupSheet, 'SetupEnableRotation'): - obj.EnableRotation = parentJob.SetupSheet.SetupEnableRotation - else: - obj.EnableRotation = 'Off' - def SetupProperties(): setup = [] setup.append("Direction") setup.append("StartSide") setup.append("StepOver") - setup.append("EnableRotation") setup.append("StartRadius") return setup diff --git a/src/Mod/Path/PathScripts/PathMillFace.py b/src/Mod/Path/PathScripts/PathMillFace.py index a932b3efbc..db01c49379 100644 --- a/src/Mod/Path/PathScripts/PathMillFace.py +++ b/src/Mod/Path/PathScripts/PathMillFace.py @@ -210,10 +210,10 @@ class ObjectFace(PathPocketBase.ObjectPocket): PathLog.debug("Processing holes and face ...") holeEnv = PathUtils.getEnvelope(partshape=holeShape, depthparams=self.depthparams) newEnv = env.cut(holeEnv) - tup = newEnv, False, 'pathMillFace', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value + tup = newEnv, False, 'pathMillFace' else: PathLog.debug("Processing solid face ...") - tup = env, False, 'pathMillFace', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value + tup = env, False, 'pathMillFace' self.removalshapes.append(tup) obj.removalshape = self.removalshapes[0][0] # save removal shape diff --git a/src/Mod/Path/PathScripts/PathPocketBaseGui.py b/src/Mod/Path/PathScripts/PathPocketBaseGui.py index 68920b0a5e..abb39c5f26 100644 --- a/src/Mod/Path/PathScripts/PathPocketBaseGui.py +++ b/src/Mod/Path/PathScripts/PathPocketBaseGui.py @@ -103,8 +103,6 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): obj.StepOver = self.form.stepOverPercent.value() if obj.OffsetPattern != str(self.form.offsetPattern.currentText()): obj.OffsetPattern = str(self.form.offsetPattern.currentText()) - if obj.EnableRotation != str(self.form.enableRotation.currentText()): - obj.EnableRotation = str(self.form.enableRotation.currentText()) PathGui.updateInputField(obj, 'ExtraOffset', self.form.extraOffset) self.updateToolController(obj, self.form.toolController) @@ -144,7 +142,6 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): self.selectInComboBox(obj.CutMode, self.form.cutMode) self.setupToolController(obj, self.form.toolController) self.setupCoolant(obj, self.form.coolantController) - self.selectInComboBox(obj.EnableRotation, self.form.enableRotation) if FeatureFacing & self.pocketFeatures(): self.selectInComboBox(obj.BoundaryShape, self.form.boundaryShape) @@ -164,7 +161,6 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): signals.append(self.form.useOutline.clicked) signals.append(self.form.minTravel.clicked) signals.append(self.form.coolantController.currentIndexChanged) - signals.append(self.form.enableRotation.currentIndexChanged) if FeatureFacing & self.pocketFeatures(): signals.append(self.form.boundaryShape.currentIndexChanged) diff --git a/src/Mod/Path/PathScripts/PathPocketShape.py b/src/Mod/Path/PathScripts/PathPocketShape.py index 68d8c29a00..4a90bf96be 100644 --- a/src/Mod/Path/PathScripts/PathPocketShape.py +++ b/src/Mod/Path/PathScripts/PathPocketShape.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # *************************************************************************** # * Copyright (c) 2017 sliptonic * -# * Copyright (c) 2020 Schildkroet * # * * # * This program is free software; you can redistribute it and/or modify * # * it under the terms of the GNU Lesser General Public License (LGPL) * @@ -26,22 +25,22 @@ import PathScripts.PathGeom as PathGeom import PathScripts.PathLog as PathLog import PathScripts.PathOp as PathOp import PathScripts.PathPocketBase as PathPocketBase -import PathScripts.PathUtils as PathUtils -import math + +from PySide import QtCore # lazily loaded modules from lazy_loader.lazy_loader import LazyLoader -Draft = LazyLoader('Draft', globals(), 'Draft') Part = LazyLoader('Part', globals(), 'Part') TechDraw = LazyLoader('TechDraw', globals(), 'TechDraw') +math = LazyLoader('math', globals(), 'math') -from PySide import QtCore __title__ = "Path Pocket Shape Operation" __author__ = "sliptonic (Brad Collette)" __url__ = "https://www.freecadweb.org" __doc__ = "Class and implementation of shape based Pocket operation." + PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) # PathLog.trackModule(PathLog.thisModule()) @@ -62,7 +61,6 @@ def endPoints(edgeOrWire): cnt = len([p2 for p2 in pts if PathGeom.pointsCoincide(p, p2)]) if 1 == cnt: unique.append(p) - return unique pfirst = edgeOrWire.valueAt(edgeOrWire.FirstParameter) @@ -78,7 +76,6 @@ def includesPoint(p, pts): for pt in pts: if PathGeom.pointsCoincide(p, pt): return True - return False @@ -87,12 +84,10 @@ def selectOffsetWire(feature, wires): closest = None for w in wires: dist = feature.distToShape(w)[0] - if closest is None or dist > closest[0]: # pylint: disable=unsubscriptable-object + if closest is None or dist > closest[0]: closest = (dist, w) - - if closest is not None: + if not closest is None: return closest[1] - return None @@ -104,6 +99,7 @@ def extendWire(feature, wire, length): off2D = wire.makeOffset2D(length) except FreeCAD.Base.FreeCADError: return None + endPts = endPoints(wire) if endPts: edges = [e for e in off2D.Edges if Part.Circle != type(e.Curve) or not includesPoint(e.Curve.Center, endPts)] @@ -122,15 +118,14 @@ def extendWire(feature, wire, length): edges.append(Part.Edge(Part.LineSegment(endPts[1], ePts[0]))) edges.extend(offset.Edges) edges.append(Part.Edge(Part.LineSegment(endPts[0], ePts[1]))) - return Part.Wire(edges) return None class Extension(object): - DirectionNormal = 0 - DirectionX = 1 - DirectionY = 2 + DirectionNormal = 0 + DirectionX = 1 + DirectionY = 2 def __init__(self, obj, feature, sub, length, direction): PathLog.debug("Extension(%s, %s, %s, %.2f, %s" % (obj.Label, feature, sub, length, direction)) @@ -141,8 +136,6 @@ class Extension(object): self.direction = direction self.extFaces = list() - self.wire = None - def getSubLink(self): return "%s:%s" % (self.feature, self.sub) @@ -158,7 +151,6 @@ class Extension(object): wire = Part.Wire([e0, e1, e2, e3]) self.wire = wire return wire - return extendWire(feature, Part.Wire([e0]), self.length.Value) def _getEdgeNumbers(self): @@ -166,8 +158,8 @@ class Extension(object): numbers = [nr for nr in self.sub[5:-1].split(',')] else: numbers = [self.sub[4:]] - PathLog.debug("_getEdgeNumbers() -> %s" % numbers) + return numbers def _getEdgeNames(self): @@ -181,10 +173,8 @@ class Extension(object): poffMinus = p0 - 0.01 * normal if not self.obj.Shape.isInside(poffPlus, 0.005, True): return normal - if not self.obj.Shape.isInside(poffMinus, 0.005, True): return normal.negative() - return None def _getDirection(self, wire): @@ -193,6 +183,7 @@ class Extension(object): tangent = e0.tangentAt(midparam) PathLog.track('tangent', tangent, self.feature, self.sub) normal = tangent.cross(FreeCAD.Vector(0, 0, 1)) + if PathGeom.pointsCoincide(normal, FreeCAD.Vector(0, 0, 0)): return None @@ -239,7 +230,6 @@ class Extension(object): r = circle.Radius - self.length.Value else: r = circle.Radius + self.length.Value - # assuming the offset produces a valid circle - go for it if r > 0: e3 = Part.makeCircle(r, circle.Center, circle.Axis, edge.FirstParameter * 180 / math.pi, edge.LastParameter * 180 / math.pi) @@ -261,7 +251,6 @@ class Extension(object): e0 = Part.makeLine(center, edge.valueAt(edge.FirstParameter)) e2 = Part.makeLine(edge.valueAt(edge.LastParameter), center) return Part.Wire([e0, edge, e2]) - PathLog.track() return Part.Wire([edge]) @@ -270,10 +259,8 @@ class Extension(object): direction = self._getDirection(sub) if direction is None: return None - # return self._extendEdge(feature, edge, direction) return self._extendEdge(feature, edges[0], direction) - return extendWire(feature, sub, self.length.Value) def _makeCircularExtFace(self, edge, extWire): @@ -300,12 +287,13 @@ class ObjectPocket(PathPocketBase.ObjectPocket): '''Proxy object for Pocket operation.''' def areaOpFeatures(self, obj): - return super(ObjectPocket, self).areaOpFeatures(obj) | PathOp.FeatureLocations + return super(self.__class__, self).areaOpFeatures(obj) | PathOp.FeatureLocations def initPocketOp(self, obj): '''initPocketOp(obj) ... setup receiver''' if not hasattr(obj, 'UseOutline'): obj.addProperty('App::PropertyBool', 'UseOutline', 'Pocket', QtCore.QT_TRANSLATE_NOOP('PathPocketShape', 'Uses the outline of the base geometry.')) + obj.UseOutline = False if not hasattr(obj, 'ExtensionLengthDefault'): obj.addProperty('App::PropertyDistance', 'ExtensionLengthDefault', 'Extension', QtCore.QT_TRANSLATE_NOOP('PathPocketShape', 'Default length of extensions.')) if not hasattr(obj, 'ExtensionFeature'): @@ -315,40 +303,10 @@ class ObjectPocket(PathPocketBase.ObjectPocket): obj.ExtensionCorners = True obj.setEditorMode('ExtensionFeature', 2) - self.initRotationOp(obj) - - def initRotationOp(self, obj): - '''initRotationOp(obj) ... setup receiver for rotation''' - if not hasattr(obj, 'ReverseDirection'): - obj.addProperty('App::PropertyBool', 'ReverseDirection', 'Rotation', QtCore.QT_TRANSLATE_NOOP('App::Property', 'Reverse direction of pocket operation.')) - if not hasattr(obj, 'InverseAngle'): - obj.addProperty('App::PropertyBool', 'InverseAngle', 'Rotation', QtCore.QT_TRANSLATE_NOOP('App::Property', 'Inverse the angle. Example: -22.5 -> 22.5 degrees.')) - if not hasattr(obj, 'AttemptInverseAngle'): - obj.addProperty('App::PropertyBool', 'AttemptInverseAngle', 'Rotation', QtCore.QT_TRANSLATE_NOOP('App::Property', 'Attempt the inverse angle for face access if original rotation fails.')) - if not hasattr(obj, 'LimitDepthToFace'): - obj.addProperty('App::PropertyBool', 'LimitDepthToFace', 'Rotation', QtCore.QT_TRANSLATE_NOOP('App::Property', 'Enforce the Z-depth of the selected face as the lowest value for final depth. Higher user values will be observed.')) - - def areaOpOnChanged(self, obj, prop): - '''areaOpOnChanged(obj, porp) ... process operation specific changes to properties.''' - if prop == 'EnableRotation': - self.setEditorProperties(obj) - - def setEditorProperties(self, obj): - obj.setEditorMode('ReverseDirection', 2) - if obj.EnableRotation == 'Off': - obj.setEditorMode('InverseAngle', 2) - obj.setEditorMode('AttemptInverseAngle', 2) - obj.setEditorMode('LimitDepthToFace', 2) - else: - # obj.setEditorMode('ReverseDirection', 0) - obj.setEditorMode('InverseAngle', 0) - obj.setEditorMode('AttemptInverseAngle', 0) - obj.setEditorMode('LimitDepthToFace', 0) def areaOpOnDocumentRestored(self, obj): - '''opOnDocumentRestored(obj) ... adds the UseOutline property, others, if they doesn't exist.''' + '''opOnDocumentRestored(obj) ... adds the UseOutline property if it doesn't exist.''' self.initPocketOp(obj) - self.setEditorProperties(obj) def pocketInvertExtraOffset(self): return False @@ -356,184 +314,106 @@ class ObjectPocket(PathPocketBase.ObjectPocket): def areaOpShapes(self, obj): '''areaOpShapes(obj) ... return shapes representing the solids to be removed.''' PathLog.track() - PathLog.debug("----- areaOpShapes() in PathPocketShape.py") - - self.isDebug = True if PathLog.getLevel(PathLog.thisModule()) == 4 else False - baseSubsTuples = [] - allTuples = [] - subCount = 0 if obj.Base: - PathLog.debug('Processing obj.Base') - self.removalshapes = [] # pylint: disable=attribute-defined-outside-init - - if obj.EnableRotation == 'Off': - stock = PathUtils.findParentJob(obj).Stock - for (base, subList) in obj.Base: - tup = (base, subList, 0.0, 'X', stock) - baseSubsTuples.append(tup) - else: - PathLog.debug('... Rotation is active') - # method call here - for p in range(0, len(obj.Base)): - (bst, at) = self.process_base_geometry_with_rotation(obj, p, subCount) - allTuples.extend(at) - baseSubsTuples.extend(bst) - - for o in baseSubsTuples: - self.horiz = [] # pylint: disable=attribute-defined-outside-init - self.vert = [] # pylint: disable=attribute-defined-outside-init - subBase = o[0] - subsList = o[1] - angle = o[2] - axis = o[3] - # stock = o[4] - - for sub in subsList: + PathLog.debug('base items exist. Processing...') + self.removalshapes = [] + self.horiz = [] + vertical = [] + for o in obj.Base: + PathLog.debug('Base item: {}'.format(o)) + base = o[0] + for sub in o[1]: if 'Face' in sub: - if not self.clasifySub(subBase, sub): - PathLog.error(translate('PathPocket', 'Pocket does not support shape %s.%s') % (subBase.Label, sub)) - if obj.EnableRotation != 'Off': - PathLog.warning(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 - 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) # pylint: disable=attribute-defined-outside-init - self.vWires = [TechDraw.findShapeOutline(shape, 1, FreeCAD.Vector(0, 0, 1)) for shape in self.vertical] # pylint: disable=attribute-defined-outside-init - 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) - else: - face.translate(FreeCAD.Vector(0, 0, vFinDep - face.BoundBox.ZMin)) + face = base.Shape.getElement(sub) + if type(face.Surface) == Part.Plane and PathGeom.isVertical(face.Surface.Axis): + # it's a flat horizontal face self.horiz.append(face) - - # add faces for extensions - self.exts = [] # pylint: disable=attribute-defined-outside-init - for ext in self.getExtensions(obj): - wire = ext.getWire() - if wire: - for face in ext.getExtensionFaces(wire): - self.horiz.append(face) - self.exts.append(face) - - # Place all self.horiz faces into same working plane - for h in self.horiz: - h.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - h.BoundBox.ZMin)) - - # check all faces and see if they are touching/overlapping and combine those into a compound - self.horizontal = [] # pylint: disable=attribute-defined-outside-init - for shape in PathGeom.combineConnectedShapes(self.horiz): - shape.sewShape() - # shape.tessellate(0.1) - shpZMin = shape.BoundBox.ZMin - PathLog.debug('PathGeom.combineConnectedShapes shape.BoundBox.ZMin: {}'.format(shape.BoundBox.ZMin)) - if obj.UseOutline: - wire = TechDraw.findShapeOutline(shape, 1, FreeCAD.Vector(0, 0, 1)) - wFace = Part.Face(wire) - if wFace.BoundBox.ZMin != shpZMin: - wFace.translate(FreeCAD.Vector(0, 0, shpZMin - wFace.BoundBox.ZMin)) - self.horizontal.append(wFace) - PathLog.debug('PathGeom.combineConnectedShapes shape.BoundBox.ZMin: {}'.format(wFace.BoundBox.ZMin)) - else: - self.horizontal.append(shape) - - # move all horizontal faces to FinalDepth - # extrude all faces up to StartDepth and those are the removal shapes - start_dep = obj.StartDepth.Value - clrnc = 0.5 - # self._addDebugObject('subBase', subBase.Shape) - for face in self.horizontal: - isFaceUp = True - invZ = 0.0 - useAngle = angle - faceZMin = face.BoundBox.ZMin - adj_final_dep = obj.FinalDepth.Value - trans = obj.FinalDepth.Value - face.BoundBox.ZMin - PathLog.debug('face.BoundBox.ZMin: {}'.format(face.BoundBox.ZMin)) - - if obj.EnableRotation != 'Off': - PathLog.debug('... running isFaceUp()') - isFaceUp = self.isFaceUp(subBase, face) - # Determine if face is really oriented toward Z+ (rotational purposes) - # ignore for cylindrical faces - if not isFaceUp: - PathLog.debug('... NOT isFaceUp') - useAngle += 180.0 - invZ = (-2 * face.BoundBox.ZMin) - face.translate(FreeCAD.Vector(0.0, 0.0, invZ)) - faceZMin = face.BoundBox.ZMin # reset faceZMin - PathLog.debug('... face.BoundBox.ZMin: {}'.format(face.BoundBox.ZMin)) + elif type(face.Surface) == Part.Cylinder and PathGeom.isVertical(face.Surface.Axis): + # vertical cylinder wall + if any(e.isClosed() for e in face.Edges): + # complete cylinder + circle = Part.makeCircle(face.Surface.Radius, face.Surface.Center) + disk = Part.Face(Part.Wire(circle)) + self.horiz.append(disk) + else: + # partial cylinder wall + vertical.append(face) + elif type(face.Surface) == Part.Plane and PathGeom.isHorizontal(face.Surface.Axis): + vertical.append(face) else: - PathLog.debug('... isFaceUp') - if useAngle > 180.0: - useAngle -= 360.0 + PathLog.error(translate('PathPocket', 'Pocket does not support shape %s.%s') % (base.Label, sub)) - # Apply LimitDepthToFace property for rotational operations - if obj.LimitDepthToFace: - if obj.FinalDepth.Value < face.BoundBox.ZMin: - PathLog.debug('obj.FinalDepth.Value < face.BoundBox.ZMin') - # Raise FinalDepth to face depth - adj_final_dep = faceZMin # face.BoundBox.ZMin # faceZMin - # Ensure StartDepth is above FinalDepth - if start_dep <= adj_final_dep: - start_dep = adj_final_dep + 1.0 - msg = translate('PathPocketShape', 'Start Depth is lower than face depth. Setting to:') - PathLog.warning(msg + ' {} mm.'.format(start_dep)) - PathLog.debug('LimitDepthToFace adj_final_dep: {}'.format(adj_final_dep)) - # Eif + self.vertical = PathGeom.combineConnectedShapes(vertical) + 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): + PathLog.error(translate('PathPocket', 'Vertical faces do not form a loop - ignoring')) + else: + self.horiz.append(face) - face.translate(FreeCAD.Vector(0.0, 0.0, adj_final_dep - faceZMin - clrnc)) - zExtVal = start_dep - adj_final_dep + (2 * clrnc) - extShp = face.removeSplitter().extrude(FreeCAD.Vector(0, 0, zExtVal)) - self.removalshapes.append((extShp, False, 'pathPocketShape', useAngle, axis, start_dep, adj_final_dep)) - PathLog.debug("Extent values are strDep: {}, finDep: {}, extrd: {}".format(start_dep, adj_final_dep, zExtVal)) - # Efor face - # Efor + # add faces for extensions + self.exts = [] # pylint: disable=attribute-defined-outside-init + for ext in self.getExtensions(obj): + wire = ext.getWire() + if wire: + for face in ext.getExtensionFaces(wire): + self.horiz.append(face) + self.exts.append(face) - 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 - self.outlines = [Part.Face(TechDraw.findShapeOutline(base.Shape, 1, FreeCAD.Vector(0, 0, 1))) for base in self.model] # pylint: disable=attribute-defined-outside-init + # Place all self.horiz faces into same working plane + for h in self.horiz: + h.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - h.BoundBox.ZMin)) + + # check all faces and see if they are touching/overlapping and combine those into a compound + self.horizontal = [] + for shape in PathGeom.combineConnectedShapes(self.horiz): + shape.sewShape() + shape.tessellate(0.05) # originally 0.1 + if obj.UseOutline: + wire = TechDraw.findShapeOutline(shape, 1, FreeCAD.Vector(0, 0, 1)) + wire.translate(FreeCAD.Vector(0, 0, obj.FinalDepth.Value - wire.BoundBox.ZMin)) + self.horizontal.append(Part.Face(wire)) + else: + self.horizontal.append(shape) + + # extrude all faces up to StartDepth and those are the removal shapes + extent = FreeCAD.Vector(0, 0, obj.StartDepth.Value - obj.FinalDepth.Value) + self.removalshapes = [(face.removeSplitter().extrude(extent), False) for face in self.horizontal] + + else: # process the job base object as a whole + PathLog.debug("processing the whole job base object") + self.outlines = [Part.Face(TechDraw.findShapeOutline(base.Shape, 1, FreeCAD.Vector(0, 0, 1))) for base in self.model] stockBB = self.stock.Shape.BoundBox - self.removalshapes = [] # pylint: disable=attribute-defined-outside-init - self.bodies = [] # pylint: disable=attribute-defined-outside-init + self.removalshapes = [] + self.bodies = [] for outline in self.outlines: outline.translate(FreeCAD.Vector(0, 0, stockBB.ZMin - 1)) body = outline.extrude(FreeCAD.Vector(0, 0, stockBB.ZLength + 2)) self.bodies.append(body) - self.removalshapes.append((self.stock.Shape.cut(body), False, 'pathPocketShape', 0.0, 'X', strDep, finDep)) + self.removalshapes.append((self.stock.Shape.cut(body), False)) - for (shape, hole, sub, angle, axis, strDep, finDep) in self.removalshapes: # pylint: disable=unused-variable + for (shape, hole) in self.removalshapes: shape.tessellate(0.05) # originally 0.1 if self.removalshapes: obj.removalshape = self.removalshapes[0][0] - return self.removalshapes def areaOpSetDefaultValues(self, obj, job): '''areaOpSetDefaultValues(obj, job) ... set default values''' obj.StepOver = 100 obj.ZigZagAngle = 45 - obj.ExtensionCorners = True obj.UseOutline = False - obj.ReverseDirection = False - obj.InverseAngle = False - obj.AttemptInverseAngle = True - obj.LimitDepthToFace = True + obj.ExtensionCorners = True + if job and job.Stock: + bb = job.Stock.Shape.BoundBox + obj.OpFinalDepth = bb.ZMin + obj.OpStartDepth = bb.ZMax obj.setExpression('ExtensionLengthDefault', 'OpToolDiameter / 2') def createExtension(self, obj, extObj, extFeature, extSub): @@ -553,468 +433,6 @@ class ObjectPocket(PathPocketBase.ObjectPocket): PathLog.track(obj.Label, len(extensions)) obj.ExtensionFeature = [(ext.obj, ext.getSubLink()) for ext in extensions] - def 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. - ''' - PathLog.track() - fCnt = 0 - go = True - vertLoopFace = None - tempNameList = [] - delTempNameList = 0 - 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) - wireName = 'tmpWire' + str(fCnt) - wr = Part.Wire(Part.__sortEdges__(base.Shape.getElement(sub).Edges)) - if wr.isNull(): - PathLog.debug('No wire created from {}'.format(sub)) - return (False, 0, 0) - else: - tmpWire = FreeCAD.ActiveDocument.addObject('Part::Feature', wireName).Shape = wr - tmpWireObj = FreeCAD.ActiveDocument.getObject(wireName) - tmpExtObj = FreeCAD.ActiveDocument.addObject('Part::Extrusion', extName) - tmpExt = FreeCAD.ActiveDocument.getObject(extName) - tmpExt.Base = tmpWireObj - tmpExt.DirMode = "Normal" - tmpExt.DirLink = None - tmpExt.LengthFwd = 10.0 - tmpExt.LengthRev = 0.0 - tmpExt.Solid = True - tmpExt.Reversed = False - tmpExt.Symmetric = False - tmpExt.TaperAngle = 0.0 - tmpExt.TaperAngleRev = 0.0 - - tmpExt.recompute() - tmpExt.purgeTouched() - tmpWireObj.purgeTouched() - return (True, tmpWireObj, tmpExt) - - def roundValue(precision, val): - # Convert VALxe-15 numbers to zero - if PathGeom.isRoughly(0.0, val) is True: - return 0.0 - # Convert VAL.99999999 to next integer - elif math.fabs(val % 1) > 1.0 - PathGeom.Tolerance: - return round(val) - else: - 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 - for sub in subsList: - if 'Face' in sub: - fCnt += 1 - saSum = saSum.add(base.Shape.getElement(sub).Surface.Axis) - - # Minimim of three faces required for loop to exist - if fCnt < 3: - go = False - - # Determine if all faces combined point toward loop center = False - if PathGeom.isRoughly(0, saSum.x): - if PathGeom.isRoughly(0, saSum.y): - if PathGeom.isRoughly(0, saSum.z): - PathLog.debug("Combined subs suggest loop of faces. Checking ...") - go = True - - if go is True: - lastExtrusion = None - matchList = [] - go = False - - # Cycle through subs, extruding to solid for each - for sub in subsList: - if 'Face' in sub: - fCnt += 1 - go = False - - # Extrude face to solid - (rtn, tmpWire, tmpExt) = makeTempExtrusion(base, sub, fCnt) - - # If success, record new temporary objects for deletion - if rtn is True: - tempNameList.append(tmpExt.Name) - tempNameList.append(tmpWire.Name) - delTempNameList += 1 - if lastExtrusion is None: - lastExtrusion = tmpExt - rtn = True - else: - go = False - break - - # Cycle through faces on each extrusion, looking for common normal faces for rotation analysis - if len(matchList) == 0: - for fc in lastExtrusion.Shape.Faces: - (norm, raw) = self.getFaceNormAndSurf(fc) - 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) # pylint: disable=unused-variable - 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(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(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 - # Eif - if go is False: - break - # Eif - # Eif 'Face' - # Efor - if go is True: - go = False - if len(matchList) == 2: - saTotal = FreeCAD.Vector(0.0, 0.0, 0.0) - for fc in matchList: - (norm, raw) = self.getFaceNormAndSurf(fc) - 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) - - if saTotal == FreeCAD.Vector(0.0, 0.0, 0.0): - if vertLoopFace is not None: - go = True - - if go is True: - (norm, surf) = self.getFaceNormAndSurf(vertLoopFace) - else: - PathLog.debug(translate('Path', 'Can not identify loop.')) - - if delTempNameList > 0: - for tmpNm in tempNameList: - FreeCAD.ActiveDocument.removeObject(tmpNm) - - return (go, norm, surf) - - def planarFaceFromExtrusionEdges(self, face, trans): - '''planarFaceFromExtrusionEdges(face, trans)... - Use closed edges to create a temporary face for use in the pocketing operation. - ''' - 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) - # Create planar face from edge - mFF = Part.Face(Part.Wire(Part.__sortEdges__([edg]))) - if mFF.isNull(): - PathLog.debug('Face(Part.Wire()) failed') - else: - 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) - - tmpFaceObj = FreeCAD.ActiveDocument.addObject('Part::Feature', fName).Shape = mFF - tmpFace = FreeCAD.ActiveDocument.getObject(fName) - tmpFace.purgeTouched() - - if minArea == 0.0: - minArea = tmpFace.Shape.Face1.Area - useFace = fName - planar = True - elif tmpFace.Shape.Face1.Area < minArea: - minArea = tmpFace.Shape.Face1.Area - FreeCAD.ActiveDocument.removeObject(useFace) - useFace = fName - else: - FreeCAD.ActiveDocument.removeObject(fName) - - if useFace != 'useFaceName': - self.useTempJobClones(useFace) - - return (planar, useFace) - - def clasifySub(self, bs, sub): - '''clasifySub(bs, sub)... - Given a base and a sub-feature name, returns True - if the sub-feature is a horizontally oriented flat face. - ''' - face = bs.Shape.getElement(sub) - - if type(face.Surface) == Part.Plane: - PathLog.debug('type() == Part.Plane') - if PathGeom.isVertical(face.Surface.Axis): - PathLog.debug(' -isVertical()') - # it's a flat horizontal face - self.horiz.append(face) - return True - - elif PathGeom.isHorizontal(face.Surface.Axis): - PathLog.debug(' -isHorizontal()') - self.vert.append(face) - return True - - else: - return False - - elif type(face.Surface) == Part.Cylinder and PathGeom.isVertical(face.Surface.Axis): - PathLog.debug('type() == Part.Cylinder') - # vertical cylinder wall - if any(e.isClosed() for e in face.Edges): - PathLog.debug(' -e.isClosed()') - # complete cylinder - circle = Part.makeCircle(face.Surface.Radius, face.Surface.Center) - disk = Part.Face(Part.Wire(circle)) - disk.translate(FreeCAD.Vector(0, 0, face.BoundBox.ZMin - disk.BoundBox.ZMin)) - self.horiz.append(disk) - return True - - else: - PathLog.debug(' -none isClosed()') - # partial cylinder wall - self.vert.append(face) - return True - - elif type(face.Surface) == Part.SurfaceOfExtrusion: - # extrusion wall - PathLog.debug('type() == Part.SurfaceOfExtrusion') - # Attempt to extract planar face from surface of extrusion - (planar, useFace) = self.planarFaceFromExtrusionEdges(face, trans=True) - # Save face object to self.horiz for processing or display error - if planar is True: - uFace = FreeCAD.ActiveDocument.getObject(useFace) - self.horiz.append(uFace.Shape.Faces[0]) - 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.warning(msg) - else: - PathLog.error(translate("Path", "Failed to create a planar face from edges in {}.".format(sub))) - - else: - PathLog.debug(' -type(face.Surface): {}'.format(type(face.Surface))) - return False - - # Process obj.Base with rotation enabled - def process_base_geometry_with_rotation(self, obj, p, subCount): - '''process_base_geometry_with_rotation(obj, p, subCount)... - This method is the control method for analyzing the selected features, - determining their rotational needs, and creating clones as needed - for rotational access for the pocketing operation. - - Requires the object, obj.Base index (p), and subCount reference arguments. - Returns two lists of tuples for continued processing into pocket paths. - ''' - baseSubsTuples = [] - allTuples = [] - isLoop = False - - (base, subsList) = obj.Base[p] - - # First, check all subs collectively for loop of faces - if len(subsList) > 2: - (isLoop, norm, surf) = self.checkForFacesLoop(base, subsList) - - if isLoop: - PathLog.debug("Common Surface.Axis or normalAt() value found for loop faces.") - subCount += 1 - tup = self.process_looped_sublist(obj, norm, surf) - if tup: - allTuples.append(tup) - baseSubsTuples.append(tup) - # Eif - - if not isLoop: - PathLog.debug(translate('Path', "Processing subs individually ...")) - for sub in subsList: - subCount += 1 - tup = self.process_nonloop_sublist(obj, base, sub) - if tup: - allTuples.append(tup) - baseSubsTuples.append(tup) - # Eif - - return (baseSubsTuples, allTuples) - - def process_looped_sublist(self, obj, norm, surf): - '''process_looped_sublist(obj, norm, surf)... - Process set of looped faces when rotation is enabled. - ''' - PathLog.debug(translate("Path", "Selected faces form loop. Processing looped faces.")) - rtn = False - (rtn, angle, axis, praInfo) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable - - if rtn is True: - faceNums = "" - for f in subsList: - faceNums += '_' + f.replace('Face', '') - (clnBase, angle, clnStock, tag) = self.applyRotationalAnalysis(obj, base, angle, axis, faceNums) # pylint: disable=unused-variable - - # Verify faces are correctly oriented - InverseAngle might be necessary - PathLog.debug("Checking if faces are oriented correctly after rotation.") - for sub in subsList: - face = clnBase.Shape.getElement(sub) - if type(face.Surface) == Part.Plane: - if not PathGeom.isHorizontal(face.Surface.Axis): - rtn = False - PathLog.warning(translate("PathPocketShape", "Face appears to NOT be horizontal AFTER rotation applied.")) - break - - if rtn is False: - PathLog.debug(translate("Path", "Face appears misaligned after initial rotation.") + ' 1') - if obj.InverseAngle: - (clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle) - else: - if obj.AttemptInverseAngle is True: - (clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle) - else: - msg = translate("Path", "Consider toggling the 'InverseAngle' property and recomputing.") - PathLog.warning(msg) - - if angle < 0.0: - angle += 360.0 - - tup = clnBase, subsList, angle, axis, clnStock - else: - if self.warnDisabledAxis(obj, axis) is False: - PathLog.debug("No rotation used") - axis = 'X' - angle = 0.0 - stock = PathUtils.findParentJob(obj).Stock - tup = base, subsList, angle, axis, stock - # Eif - return tup - - def process_nonloop_sublist(self, obj, base, sub): - '''process_nonloop_sublist(obj, sub)... - Process sublist with non-looped set of features when rotation is enabled. - ''' - - if sub[:4] != 'Face': - ignoreSub = base.Name + '.' + sub - PathLog.error(translate('Path', "Selected feature is not a Face. Ignoring: {}".format(ignoreSub))) - return False - - rtn = False - 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) = self.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 created: {}'.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) # pylint: disable=unused-variable - PathLog.debug("initial {}".format(praInfo)) - - clnBase = base - faceIA = clnBase.Shape.getElement(sub) - - if rtn is True: - faceNum = sub.replace('Face', '') - PathLog.debug("initial applyRotationalAnalysis") - (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) - (rtn, praAngle, praAxis, praInfo2) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable - PathLog.debug("follow-up {}".format(praInfo2)) - - isFaceUp = self.isFaceUp(clnBase, faceIA) - if isFaceUp: - rtn = False - - if round(abs(praAngle), 8) == 180.0: - rtn = False - if not isFaceUp: - PathLog.debug('initial isFaceUp is False') - angle = 0.0 - # Eif - - if rtn: - # initial rotation failed, attempt inverse rotation if user requests it - PathLog.debug(translate("Path", "Face appears misaligned after initial rotation.") + ' 2') - if obj.AttemptInverseAngle: - PathLog.debug(translate("Path", "Applying inverse angle automatically.")) - (clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle) - else: - if obj.InverseAngle: - PathLog.debug(translate("Path", "Applying inverse angle manually.")) - (clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle) - else: - msg = translate("Path", "Consider toggling the 'InverseAngle' property and recomputing.") - PathLog.warning(msg) - - faceIA = clnBase.Shape.getElement(sub) - if not self.isFaceUp(clnBase, faceIA): - angle += 180.0 - - # Normalize rotation angle - if angle < 0.0: - angle += 360.0 - elif angle > 360.0: - angle -= 360.0 - - return (clnBase, [sub], angle, axis, clnStock) - - if not self.warnDisabledAxis(obj, axis): - PathLog.debug(str(sub) + ": No rotation used") - axis = 'X' - angle = 0.0 - stock = PathUtils.findParentJob(obj).Stock - return (base, [sub], angle, axis, stock) - - # Method to add temporary debug object - def _addDebugObject(self, objName, objShape): - '''_addDebugObject(objName, objShape)... - Is passed a desired debug object's desired name and shape. - This method creates a FreeCAD object for debugging purposes. - The created object must be deleted manually from the object tree - by the user. - ''' - if self.isDebug: - O = FreeCAD.ActiveDocument.addObject('Part::Feature', 'debug_' + objName) - O.Shape = objShape - O.purgeTouched() - def SetupProperties(): setup = PathPocketBase.SetupProperties() @@ -1022,10 +440,6 @@ def SetupProperties(): setup.append('ExtensionLengthDefault') setup.append('ExtensionFeature') setup.append('ExtensionCorners') - setup.append("ReverseDirection") - setup.append("InverseAngle") - setup.append("AttemptInverseAngle") - setup.append("LimitDepthToFace") return setup @@ -1033,5 +447,7 @@ def Create(name, obj=None): '''Create(name) ... Creates and returns a Pocket operation.''' if obj is None: obj = FreeCAD.ActiveDocument.addObject('Path::FeaturePython', name) + obj.Proxy = ObjectPocket(obj, name) - return obj + + return obj \ No newline at end of file diff --git a/src/Mod/Path/PathScripts/PathProfile.py b/src/Mod/Path/PathScripts/PathProfile.py index 173aaaa401..f1ee7ce7b5 100644 --- a/src/Mod/Path/PathScripts/PathProfile.py +++ b/src/Mod/Path/PathScripts/PathProfile.py @@ -118,15 +118,6 @@ class ObjectProfile(PathAreaOp.ObjectOp): QtCore.QT_TRANSLATE_NOOP("App::Property", "Side of edge that tool should cut")), ("App::PropertyBool", "UseComp", "Profile", QtCore.QT_TRANSLATE_NOOP("App::Property", "Make True, if using Cutter Radius Compensation")), - - ("App::PropertyBool", "ReverseDirection", "Rotation", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Reverse direction of pocket operation.")), - ("App::PropertyBool", "InverseAngle", "Rotation", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Inverse the angle. Example: -22.5 -> 22.5 degrees.")), - ("App::PropertyBool", "AttemptInverseAngle", "Rotation", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Attempt the inverse angle for face access if original rotation fails.")), - ("App::PropertyBool", "LimitDepthToFace", "Rotation", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Enforce the Z-depth of the selected face as the lowest value for final depth. Higher user values will be observed.")) ] def areaOpPropertyEnumerations(self): @@ -144,15 +135,11 @@ class ObjectProfile(PathAreaOp.ObjectOp): '''areaOpPropertyDefaults(obj, job) ... returns a dictionary of default values for the operation's properties.''' return { - 'AttemptInverseAngle': True, 'Direction': 'CW', 'HandleMultipleFeatures': 'Collectively', - 'InverseAngle': False, 'JoinType': 'Round', - 'LimitDepthToFace': True, 'MiterLimit': 0.1, 'OffsetExtra': 0.0, - 'ReverseDirection': False, 'Side': 'Outside', 'UseComp': True, 'processCircles': False, @@ -186,7 +173,6 @@ class ObjectProfile(PathAreaOp.ObjectOp): '''setOpEditorProperties(obj, porp) ... Process operation-specific changes to properties visibility.''' fc = 2 # ml = 0 if obj.JoinType == 'Miter' else 2 - rotation = 2 if obj.EnableRotation == 'Off' else 0 side = 0 if obj.UseComp else 2 opType = self._getOperationType(obj) @@ -199,18 +185,12 @@ class ObjectProfile(PathAreaOp.ObjectOp): obj.setEditorMode('JoinType', 2) obj.setEditorMode('MiterLimit', 2) # ml - obj.setEditorMode('Side', side) obj.setEditorMode('HandleMultipleFeatures', fc) obj.setEditorMode('processCircles', fc) obj.setEditorMode('processHoles', fc) obj.setEditorMode('processPerimeter', fc) - obj.setEditorMode('ReverseDirection', rotation) - obj.setEditorMode('InverseAngle', rotation) - obj.setEditorMode('AttemptInverseAngle', rotation) - obj.setEditorMode('LimitDepthToFace', rotation) - def _getOperationType(self, obj): if len(obj.Base) == 0: return 'Contour' @@ -228,7 +208,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): def areaOpOnChanged(self, obj, prop): '''areaOpOnChanged(obj, prop) ... updates certain property visibilities depending on changed properties.''' - if prop in ['UseComp', 'JoinType', 'EnableRotation', 'Base']: + if prop in ['UseComp', 'JoinType', 'Base']: if hasattr(self, 'propertiesReady') and self.propertiesReady: self.setOpEditorProperties(obj) @@ -295,10 +275,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): PathLog.track() shapes = [] - baseSubsTuples = list() - allTuples = list() remainingObjBaseFeatures = list() - subCount = 0 self.isDebug = True if PathLog.getLevel(PathLog.thisModule()) == 4 else False self.inaccessibleMsg = translate('PathProfile', 'The selected edge(s) are inaccessible. If multiple, re-ordering selection might work.') self.offsetExtra = obj.OffsetExtra.Value @@ -330,39 +307,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): # Edges were already processed, or whole model targeted. PathLog.debug("remainingObjBaseFeatures is False") elif remainingObjBaseFeatures and len(remainingObjBaseFeatures) > 0: # Process remaining features after edges processed above. - if obj.EnableRotation != 'Off': - for p in range(0, len(remainingObjBaseFeatures)): - (base, subsList) = remainingObjBaseFeatures[p] - for sub in subsList: - subCount += 1 - shape = getattr(base.Shape, sub) - if isinstance(shape, Part.Face): - tup = self._analyzeFace(obj, base, sub, shape, subCount) - allTuples.append(tup) - - if subCount > 1 and obj.HandleMultipleFeatures == 'Collectively': - msg = translate('PathProfile', "Multiple faces in Base Geometry.") + " " - msg += translate('PathProfile', "Depth settings will be applied to all faces.") - FreeCAD.Console.PrintWarning(msg) - - (Tags, Grps) = self.sortTuplesByIndex(allTuples, 2) # return (TagList, GroupList) - subList = [] - for o in range(0, len(Tags)): - subList = [] - for (base, sub, _, angle, axis, stock) in Grps[o]: - subList.append(sub) - - pair = base, subList, angle, axis, stock - baseSubsTuples.append(pair) - # Efor - else: - stock = PathUtils.findParentJob(obj).Stock - for (base, subList) in remainingObjBaseFeatures: - baseSubsTuples.append((base, subList, 0.0, 'X', stock)) - # Eif - - # for base in remainingObjBaseFeatures: - for (base, subsList, angle, axis, stock) in baseSubsTuples: + for (base, subsList) in remainingObjBaseFeatures: holes = [] faces = [] faceDepths = [] @@ -383,31 +328,24 @@ class ObjectProfile(PathAreaOp.ObjectOp): msg = translate('PathProfile', "Found a selected object which is not a face. Ignoring:") PathLog.warning(msg + " {}".format(ignoreSub)) - # Identify initial Start and Final Depths - finDep = obj.FinalDepth.Value - strDep = obj.StartDepth.Value - for baseShape, wire in holes: cont = False f = Part.makeFace(wire, 'Part::FaceMakerSimple') drillable = PathUtils.isDrillable(baseShape, wire) - ot = self._openingType(obj, baseShape, f, strDep, finDep) if obj.processCircles: if drillable: - if ot < 1: - cont = True + cont = True if obj.processHoles: if not drillable: - if ot < 1: - cont = True + cont = True + if cont: shapeEnv = PathUtils.getEnvelope(baseShape, subshape=f, depthparams=self.depthparams) if shapeEnv: self._addDebugObject('HoleShapeEnvelope', shapeEnv) - # env = PathUtils.getEnvelope(baseShape, subshape=f, depthparams=self.depthparams) - tup = shapeEnv, True, 'pathProfile', angle, axis, strDep, finDep + tup = shapeEnv, True, 'pathProfile' shapes.append(tup) if faces and obj.processPerimeter: @@ -416,11 +354,6 @@ class ObjectProfile(PathAreaOp.ObjectOp): cont = True profileshape = Part.makeCompound(faces) - if obj.LimitDepthToFace is True and obj.EnableRotation != 'Off': - if profileshape.BoundBox.ZMin > obj.FinalDepth.Value: - finDep = profileshape.BoundBox.ZMin - custDepthparams = self._customDepthParams(obj, strDep + 0.5, finDep) # only an envelope - try: shapeEnv = PathUtils.getEnvelope(profileshape, depthparams=custDepthparams) except Exception as ee: # pylint: disable=broad-except @@ -431,18 +364,17 @@ class ObjectProfile(PathAreaOp.ObjectOp): if cont: self._addDebugObject('CollectCutShapeEnv', shapeEnv) - tup = shapeEnv, False, 'pathProfile', angle, axis, strDep, finDep + tup = shapeEnv, False, 'pathProfile' shapes.append(tup) elif obj.HandleMultipleFeatures == 'Individually': for shape in faces: - finalDep = obj.FinalDepth.Value custDepthparams = self.depthparams - self._addDebugObject('Rotation_Indiv_Shp', shape) + self._addDebugObject('Indiv_Shp', shape) shapeEnv = PathUtils.getEnvelope(shape, depthparams=custDepthparams) if shapeEnv: self._addDebugObject('IndivCutShapeEnv', shapeEnv) - tup = shapeEnv, False, 'pathProfile', angle, axis, strDep, finalDep + tup = shapeEnv, False, 'pathProfile' shapes.append(tup) else: # Try to build targets from the job models @@ -461,7 +393,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): if (drillable and obj.processCircles) or (not drillable and obj.processHoles): f = Part.makeFace(wire, 'Part::FaceMakerSimple') env = PathUtils.getEnvelope(self.model[0].Shape, subshape=f, depthparams=self.depthparams) - tup = env, True, 'pathProfile', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value + tup = env, True, 'pathProfile' shapes.append(tup) # Process perimeter if requested by user @@ -470,7 +402,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): for wire in shape.Wires: f = Part.makeFace(wire, 'Part::FaceMakerSimple') env = PathUtils.getEnvelope(self.model[0].Shape, subshape=f, depthparams=self.depthparams) - tup = env, False, 'pathProfile', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value + tup = env, False, 'pathProfile' shapes.append(tup) else: # shapes.extend([(PathUtils.getEnvelope(partshape=base.Shape, subshape=None, depthparams=self.depthparams), False) for base in self.model if hasattr(base, 'Shape')]) @@ -492,93 +424,6 @@ class ObjectProfile(PathAreaOp.ObjectOp): return shapes - # Analyze a face for rotational needs - def _analyzeFace(self, obj, base, sub, shape, subCount): - rtn = False - (norm, surf) = self.getFaceNormAndSurf(shape) - (rtn, angle, axis, praInfo) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable - PathLog.debug("initial faceRotationAnalysis: {}".format(praInfo)) - - if rtn is True: - # Rotational alignment is suggested from analysis - (clnBase, angle, clnStock, tag) = self.applyRotationalAnalysis(obj, base, angle, axis, subCount) - # Verify faces are correctly oriented - InverseAngle might be necessary - faceIA = getattr(clnBase.Shape, sub) - (norm, surf) = self.getFaceNormAndSurf(faceIA) - (rtn, praAngle, praAxis, praInfo2) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable - PathLog.debug("follow-up faceRotationAnalysis: {}".format(praInfo2)) - PathLog.debug("praAngle: {}".format(praAngle)) - - if abs(praAngle) == 180.0: - rtn = False - if self.isFaceUp(clnBase, faceIA) is False: - PathLog.debug('isFaceUp 1 is False') - angle -= 180.0 - - if rtn is True: - PathLog.debug(translate("Path", "Face appears misaligned after initial rotation.")) - if obj.AttemptInverseAngle is True: - PathLog.debug(translate("Path", "Applying inverse angle automatically.")) - (clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle) - else: - if obj.InverseAngle: - PathLog.debug(translate("Path", "Applying inverse angle manually.")) - (clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle) - else: - msg = translate("Path", "Consider toggling the 'InverseAngle' property and recomputing.") - PathLog.warning(msg) - - if self.isFaceUp(clnBase, faceIA) is False: - PathLog.debug('isFaceUp 2 is False') - angle += 180.0 - else: - PathLog.debug(' isFaceUp') - - else: - PathLog.debug("Face appears to be oriented correctly.") - - if angle < 0.0: - angle += 360.0 - - tup = clnBase, sub, tag, angle, axis, clnStock - else: - 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('.', '_') - stock = PathUtils.findParentJob(obj).Stock - tup = base, sub, tag, angle, axis, stock - - return tup - - def _openingType(self, obj, baseShape, face, strDep, finDep): - # Test if solid geometry above opening - extDistPos = strDep - face.BoundBox.ZMin - if extDistPos > 0: - extFacePos = face.extrude(FreeCAD.Vector(0.0, 0.0, extDistPos)) - cmnPos = baseShape.common(extFacePos) - if cmnPos.Volume > 0: - # Signifies solid protrusion above, - # or overhang geometry above opening - return 1 - # Test if solid geometry below opening - extDistNeg = finDep - face.BoundBox.ZMin - if extDistNeg < 0: - extFaceNeg = face.extrude(FreeCAD.Vector(0.0, 0.0, extDistNeg)) - cmnNeg = baseShape.common(extFaceNeg) - if cmnNeg.Volume == 0: - # No volume below signifies - # an unobstructed/nonconstricted opening through baseShape - return 0 - else: - # Could be a pocket, - # or a constricted/narrowing hole through baseShape - return -1 - msg = translate('PathProfile', 'failed to return opening type.') - PathLog.debug('_openingType() ' + msg) - return -2 - # Method to handle each model as a whole, when no faces are selected def _processEachModel(self, obj): shapeTups = list() @@ -628,7 +473,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): if f: shapeEnv = PathUtils.getEnvelope(Part.Face(f), depthparams=self.depthparams) if shapeEnv: - tup = shapeEnv, False, 'Profile', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value + tup = shapeEnv, False, 'pathProfile' shapes.append(tup) else: PathLog.error(self.inaccessibleMsg) @@ -662,7 +507,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): PathLog.error(self.inaccessibleMsg) if openEdges: - tup = openEdges, False, 'OpenEdge', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value + tup = openEdges, False, 'OpenEdge' shapes.append(tup) else: if zDiff < self.JOB.GeometryTolerance.Value: diff --git a/src/Mod/Path/PathScripts/PathProfileGui.py b/src/Mod/Path/PathScripts/PathProfileGui.py index 8a85a6abfa..a7f75b99e9 100644 --- a/src/Mod/Path/PathScripts/PathProfileGui.py +++ b/src/Mod/Path/PathScripts/PathProfileGui.py @@ -77,8 +77,6 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): if obj.Direction != str(self.form.direction.currentText()): obj.Direction = str(self.form.direction.currentText()) PathGui.updateInputField(obj, 'OffsetExtra', self.form.extraOffset) - if obj.EnableRotation != str(self.form.enableRotation.currentText()): - obj.EnableRotation = str(self.form.enableRotation.currentText()) if obj.UseComp != self.form.useCompensation.isChecked(): obj.UseComp = self.form.useCompensation.isChecked() @@ -100,7 +98,6 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): self.selectInComboBox(obj.Side, self.form.cutSide) self.selectInComboBox(obj.Direction, self.form.direction) self.form.extraOffset.setText(FreeCAD.Units.Quantity(obj.OffsetExtra.Value, FreeCAD.Units.Length).UserString) - self.selectInComboBox(obj.EnableRotation, self.form.enableRotation) self.form.useCompensation.setChecked(obj.UseComp) self.form.useStartPoint.setChecked(obj.UseStartPoint) @@ -118,7 +115,6 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): signals.append(self.form.cutSide.currentIndexChanged) signals.append(self.form.direction.currentIndexChanged) signals.append(self.form.extraOffset.editingFinished) - signals.append(self.form.enableRotation.currentIndexChanged) signals.append(self.form.useCompensation.stateChanged) signals.append(self.form.useStartPoint.stateChanged) signals.append(self.form.processHoles.stateChanged) diff --git a/src/Mod/Path/PathScripts/PathThreadMilling.py b/src/Mod/Path/PathScripts/PathThreadMilling.py index 4aa45480d4..f7ca04b19a 100644 --- a/src/Mod/Path/PathScripts/PathThreadMilling.py +++ b/src/Mod/Path/PathScripts/PathThreadMilling.py @@ -195,12 +195,6 @@ class ObjectThreadMilling(PathCircularHoleBase.ObjectOp): obj.addProperty("App::PropertyLink", "ClearanceOp", "Operation", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Operation to clear the inside of the thread")) obj.Direction = self.Directions - # Rotation related properties - if not hasattr(obj, 'EnableRotation'): - obj.addProperty("App::PropertyEnumeration", "EnableRotation", "Rotation", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable rotation to gain access to pockets/areas not normal to Z axis.")) - obj.EnableRotation = ['Off', 'A(x)', 'B(y)', 'A & B'] - - def threadStartDepth(self, obj): if obj.ThreadOrientation == self.RightHand: if obj.Direction == self.DirectionClimb: diff --git a/src/Mod/Path/PathScripts/PathWaterline.py b/src/Mod/Path/PathScripts/PathWaterline.py index 34d1abffb8..764d4c723b 100644 --- a/src/Mod/Path/PathScripts/PathWaterline.py +++ b/src/Mod/Path/PathScripts/PathWaterline.py @@ -251,8 +251,6 @@ class ObjectWaterline(PathOp.ObjectOp): # Used to hide inputs in properties list expMode = G = 0 show = hide = A = B = C = 2 - if hasattr(obj, 'EnableRotation'): - obj.setEditorMode('EnableRotation', hide) obj.setEditorMode('BoundaryEnforcement', hide) obj.setEditorMode('InternalFeaturesAdjustment', hide)