From cb233835264f58d70ef41eeaa3e2ea922bd4c0da Mon Sep 17 00:00:00 2001 From: sliptonic Date: Sat, 15 Jun 2024 09:30:53 -0500 Subject: [PATCH] Fixing 'red ink' bugs in CAM fixes: https://github.com/Ondsel-Development/FreeCAD/issues/93 fixes https://github.com/Ondsel-Development/FreeCAD/issues/87 fixes https://github.com/Ondsel-Development/FreeCAD/issues/88 fix threadmilling error if not proper tool fix vcarve error if no proper tool fix probe error if no proper tool fix deburr error if no base geometry Fix adaptive op error if no base geometry --- src/Mod/CAM/App/Area.cpp | 4 +- src/Mod/CAM/Path/Main/Sanity/Sanity.py | 6 ++- src/Mod/CAM/Path/Op/Adaptive.py | 6 ++- src/Mod/CAM/Path/Op/Area.py | 48 +++++++++++++++++---- src/Mod/CAM/Path/Op/Base.py | 5 +-- src/Mod/CAM/Path/Op/Deburr.py | 8 ++-- src/Mod/CAM/Path/Op/Gui/Base.py | 31 ++++++------- src/Mod/CAM/Path/Op/Gui/FeatureExtension.py | 2 +- src/Mod/CAM/Path/Op/Gui/Probe.py | 13 +++++- src/Mod/CAM/Path/Op/Gui/ThreadMilling.py | 14 +++++- src/Mod/CAM/Path/Op/Gui/Vcarve.py | 15 +++++-- src/Mod/CAM/Path/Op/PocketBase.py | 5 +++ src/Mod/CAM/Path/Op/Probe.py | 9 ++++ src/Mod/CAM/Path/Op/Slot.py | 10 ++--- src/Mod/CAM/Path/Op/Vcarve.py | 12 ++++-- 15 files changed, 134 insertions(+), 54 deletions(-) diff --git a/src/Mod/CAM/App/Area.cpp b/src/Mod/CAM/App/Area.cpp index 309c5ad84f..273f0fb3b6 100644 --- a/src/Mod/CAM/App/Area.cpp +++ b/src/Mod/CAM/App/Area.cpp @@ -1639,7 +1639,9 @@ std::vector > Area::makeSections( if (hitMin) continue; hitMin = true; double zNew = zMin + myParams.SectionTolerance; - AREA_WARN("hit bottom " << z << ',' << zMin << ',' << zNew); + //Silence the warning if _heights is not empty + if (_heights.empty() && FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) + AREA_WARN("hit bottom " << z << ',' << zMin << ',' << zNew); z = zNew; } else if (zMax - z < myParams.SectionTolerance) { diff --git a/src/Mod/CAM/Path/Main/Sanity/Sanity.py b/src/Mod/CAM/Path/Main/Sanity/Sanity.py index 046bedcb5c..7749be2ddc 100644 --- a/src/Mod/CAM/Path/Main/Sanity/Sanity.py +++ b/src/Mod/CAM/Path/Main/Sanity/Sanity.py @@ -37,6 +37,7 @@ import Path.Main.Sanity.ImageBuilder as ImageBuilder import Path.Main.Sanity.ReportGenerator as ReportGenerator import os import tempfile +import Path.Dressup.Utils as PathDressup translate = FreeCAD.Qt.translate @@ -454,11 +455,12 @@ class CAMSanity: used = False for op in obj.Operations.Group: - if hasattr(op, "ToolController") and op.ToolController is TC: + base_op = PathDressup.baseOp(op) + if hasattr(base_op, "ToolController") and base_op.ToolController is TC: used = True tooldata.setdefault("ops", []).append( { - "Operation": op.Label, + "Operation": base_op.Label, "ToolController": TC.Label, "Feed": str(TC.HorizFeed), "Speed": str(TC.SpindleSpeed), diff --git a/src/Mod/CAM/Path/Op/Adaptive.py b/src/Mod/CAM/Path/Op/Adaptive.py index 6eb2d7f3e0..420f025744 100644 --- a/src/Mod/CAM/Path/Op/Adaptive.py +++ b/src/Mod/CAM/Path/Op/Adaptive.py @@ -678,7 +678,11 @@ def Execute(op, obj): # Get list of working edges for adaptive algorithm pathArray = op.pathArray if not pathArray: - Path.Log.error("No wire data returned.") + msg = translate( + "CAM", + "Adaptive operation couldn't determine the boundary wire. Did you select base geometry?", + ) + FreeCAD.Console.PrintUserWarning(msg) return path2d = convertTo2d(pathArray) diff --git a/src/Mod/CAM/Path/Op/Area.py b/src/Mod/CAM/Path/Op/Area.py index 541fc19d6f..559f454248 100644 --- a/src/Mod/CAM/Path/Op/Area.py +++ b/src/Mod/CAM/Path/Op/Area.py @@ -223,7 +223,9 @@ class ObjectOp(PathOp.ObjectOp): area.add(baseobject) areaParams = self.areaOpAreaParams(obj, isHole) - areaParams["SectionTolerance"] = FreeCAD.Base.Precision.confusion() * 10 # basically 1e-06 + areaParams["SectionTolerance"] = ( + FreeCAD.Base.Precision.confusion() * 10 + ) # basically 1e-06 heights = [i for i in self.depthparams] Path.Log.debug("depths: {}".format(heights)) @@ -238,7 +240,9 @@ class ObjectOp(PathOp.ObjectOp): Path.Log.debug("sections = %s" % sections) # Rest machining - self.sectionShapes = self.sectionShapes + [section.toTopoShape() for section in sections] + self.sectionShapes = self.sectionShapes + [ + section.toTopoShape() for section in sections + ] if hasattr(obj, "UseRestMachining") and obj.UseRestMachining: restSections = [] for section in sections: @@ -246,15 +250,36 @@ class ObjectOp(PathOp.ObjectOp): z = bbox.ZMin sectionClearedAreas = [] for op in self.job.Operations.Group: - if self in [x.Proxy for x in [op] + op.OutListRecursive if hasattr(x, "Proxy")]: + if self in [ + x.Proxy + for x in [op] + op.OutListRecursive + if hasattr(x, "Proxy") + ]: break if hasattr(op, "Active") and op.Active and op.Path: - tool = op.Proxy.tool if hasattr(op.Proxy, "tool") else op.ToolController.Proxy.getTool(op.ToolController) + tool = ( + op.Proxy.tool + if hasattr(op.Proxy, "tool") + else op.ToolController.Proxy.getTool(op.ToolController) + ) diameter = tool.Diameter.getValueAs("mm") - dz = 0 if not hasattr(tool, "TipAngle") else -PathUtils.drillTipLength(tool) # for drills, dz translates to the full width part of the tool - sectionClearedAreas.append(section.getClearedArea(op.Path, diameter, z+dz+self.job.GeometryTolerance.getValueAs("mm"), bbox)) - restSection = section.getRestArea(sectionClearedAreas, self.tool.Diameter.getValueAs("mm")) - if (restSection is not None): + dz = ( + 0 + if not hasattr(tool, "TipAngle") + else -PathUtils.drillTipLength(tool) + ) # for drills, dz translates to the full width part of the tool + sectionClearedAreas.append( + section.getClearedArea( + op.Path, + diameter, + z + dz + self.job.GeometryTolerance.getValueAs("mm"), + bbox, + ) + ) + restSection = section.getRestArea( + sectionClearedAreas, self.tool.Diameter.getValueAs("mm") + ) + if restSection is not None: restSections.append(restSection) sections = restSections @@ -273,7 +298,12 @@ class ObjectOp(PathOp.ObjectOp): pathParams["preamble"] = False # disable path sorting for offset and zigzag-offset paths - if hasattr(obj, "OffsetPattern") and obj.OffsetPattern in ["ZigZagOffset", "Offset"] and hasattr(obj, "MinTravel") and not obj.MinTravel: + if ( + hasattr(obj, "OffsetPattern") + and obj.OffsetPattern in ["ZigZagOffset", "Offset"] + and hasattr(obj, "MinTravel") + and not obj.MinTravel + ): pathParams["sort_mode"] = 0 if not self.areaOpRetractTool(obj): diff --git a/src/Mod/CAM/Path/Op/Base.py b/src/Mod/CAM/Path/Op/Base.py index 2600d4edda..64f7a824e0 100644 --- a/src/Mod/CAM/Path/Op/Base.py +++ b/src/Mod/CAM/Path/Op/Base.py @@ -912,10 +912,7 @@ class ObjectOp(object): obj.Base = baselist else: Path.Log.notice( - ( - translate("CAM", "Base object %s.%s rejected by operation") - + "\n" - ) + (translate("CAM", "Base object %s.%s rejected by operation") + "\n") % (base.Label, sub) ) diff --git a/src/Mod/CAM/Path/Op/Deburr.py b/src/Mod/CAM/Path/Op/Deburr.py index 68860da4d2..14bf268915 100644 --- a/src/Mod/CAM/Path/Op/Deburr.py +++ b/src/Mod/CAM/Path/Op/Deburr.py @@ -157,9 +157,7 @@ class ObjectDeburr(PathEngraveBase.ObjectOp): "App::PropertyInteger", "EntryPoint", "Deburr", - QT_TRANSLATE_NOOP( - "App::Property", "The segment where the toolpath starts" - ), + QT_TRANSLATE_NOOP("App::Property", "The segment where the toolpath starts"), ) ENUMS = self.propertyEnumerations() @@ -210,6 +208,10 @@ class ObjectDeburr(PathEngraveBase.ObjectOp): def opExecute(self, obj): Path.Log.track(obj.Label) + + if not obj.Base: + return + if not hasattr(self, "printInfo"): self.printInfo = True try: diff --git a/src/Mod/CAM/Path/Op/Gui/Base.py b/src/Mod/CAM/Path/Op/Gui/Base.py index 2793b6fcbe..2e82e3908e 100644 --- a/src/Mod/CAM/Path/Op/Gui/Base.py +++ b/src/Mod/CAM/Path/Op/Gui/Base.py @@ -213,6 +213,14 @@ class TaskPanelPage(object): if self._installTCUpdate(): PathJob.Notification.updateTC.connect(self.resetToolController) + def show_error_message(self, title, message): + msg_box = QtGui.QMessageBox() + msg_box.setIcon(QtGui.QMessageBox.Critical) + msg_box.setWindowTitle(title) + msg_box.setText(message) + msg_box.setStandardButtons(QtGui.QMessageBox.Ok) + msg_box.exec_() + def _installTCUpdate(self): return hasattr(self.form, "toolController") @@ -545,7 +553,6 @@ class TaskPanelBaseGeometryPage(TaskPanelPage): "PathOp", "Please select %s from a single solid" % self.featureName(), ) - FreeCAD.Console.PrintError(msg + "\n") Path.Log.debug(msg) return False sel = selection[0] @@ -554,32 +561,19 @@ class TaskPanelBaseGeometryPage(TaskPanelPage): not self.supportsVertexes() and selection[0].SubObjects[0].ShapeType == "Vertex" ): - if not ignoreErrors: - Path.Log.error(translate("PathOp", "Vertexes are not supported")) return False if ( not self.supportsEdges() and selection[0].SubObjects[0].ShapeType == "Edge" ): - if not ignoreErrors: - Path.Log.error(translate("PathOp", "Edges are not supported")) return False if ( not self.supportsFaces() and selection[0].SubObjects[0].ShapeType == "Face" ): - if not ignoreErrors: - Path.Log.error(translate("PathOp", "Faces are not supported")) return False else: if not self.supportsPanels() or "Panel" not in sel.Object.Name: - if not ignoreErrors: - Path.Log.error( - translate( - "PathOp", - "Please select %s of a solid" % self.featureName(), - ) - ) return False return True @@ -622,9 +616,6 @@ class TaskPanelBaseGeometryPage(TaskPanelPage): Path.Log.debug("Setting new base: %s -> %s" % (self.obj.Base, newlist)) self.obj.Base = newlist - # self.obj.Proxy.execute(self.obj) - # FreeCAD.ActiveDocument.recompute() - def clearBase(self): self.obj.Base = [] self.setDirty() @@ -1274,7 +1265,11 @@ class TaskPanel(object): def getStandardButtons(self): """getStandardButtons() ... returns the Buttons for the task panel.""" - return QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Apply | QtGui.QDialogButtonBox.Cancel + return ( + QtGui.QDialogButtonBox.Ok + | QtGui.QDialogButtonBox.Apply + | QtGui.QDialogButtonBox.Cancel + ) def setupUi(self): """setupUi() ... internal function to initialise all pages.""" diff --git a/src/Mod/CAM/Path/Op/Gui/FeatureExtension.py b/src/Mod/CAM/Path/Op/Gui/FeatureExtension.py index 81f184d5f6..75cb156ad0 100644 --- a/src/Mod/CAM/Path/Op/Gui/FeatureExtension.py +++ b/src/Mod/CAM/Path/Op/Gui/FeatureExtension.py @@ -227,7 +227,7 @@ class TaskPanelExtensionPage(PathOpGui.TaskPanelPage): def cleanupPage(self, obj): try: self.obj.ViewObject.RootNode.removeChild(self.switch) - except ReferenceError: + except (ReferenceError, RuntimeError) as e: Path.Log.debug("obj already destroyed - no cleanup required") def getForm(self): diff --git a/src/Mod/CAM/Path/Op/Gui/Probe.py b/src/Mod/CAM/Path/Op/Gui/Probe.py index c7b748fb05..618ad00067 100644 --- a/src/Mod/CAM/Path/Op/Gui/Probe.py +++ b/src/Mod/CAM/Path/Op/Gui/Probe.py @@ -27,6 +27,7 @@ import Path.Base.Gui.Util as PathGuiUtil import Path.Op.Gui.Base as PathOpGui import Path.Op.Probe as PathProbe import PathGui +import PathScripts.PathUtils as PathUtils from PySide.QtCore import QT_TRANSLATE_NOOP from PySide import QtCore, QtGui @@ -55,13 +56,23 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): def getFields(self, obj): """getFields(obj) ... transfers values from UI to obj's properties""" - self.updateToolController(obj, self.form.toolController) PathGuiUtil.updateInputField(obj, "Xoffset", self.form.Xoffset) PathGuiUtil.updateInputField(obj, "Yoffset", self.form.Yoffset) obj.PointCountX = self.form.PointCountX.value() obj.PointCountY = self.form.PointCountY.value() obj.OutputFileName = str(self.form.OutputFileName.text()) + try: + self.updateToolController(obj, self.form.toolController) + except PathUtils.PathNoTCExistsException: + title = translate("CAM", "No valid toolcontroller") + message = translate( + "CAM", + "This operation requires a tool controller with a probe tool", + ) + + self.show_error_message(title, message) + def setFields(self, obj): """setFields(obj) ... transfers obj's property values to UI""" self.setupToolController(obj, self.form.toolController) diff --git a/src/Mod/CAM/Path/Op/Gui/ThreadMilling.py b/src/Mod/CAM/Path/Op/Gui/ThreadMilling.py index 8acda6cfd2..9f99e72e05 100644 --- a/src/Mod/CAM/Path/Op/Gui/ThreadMilling.py +++ b/src/Mod/CAM/Path/Op/Gui/ThreadMilling.py @@ -28,12 +28,13 @@ import Path.Op.Gui.Base as PathOpGui import Path.Op.Gui.CircularHoleBase as PathCircularHoleBaseGui import Path.Op.ThreadMilling as PathThreadMilling import PathGui +import PathScripts.PathUtils as PathUtils import csv from PySide.QtCore import QT_TRANSLATE_NOOP +from PySide import QtCore, QtGui -from PySide import QtCore __title__ = "CAM Thread Milling Operation UI." __author__ = "sliptonic (Brad Collette)" @@ -112,7 +113,16 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage): obj.LeadInOut = self.form.leadInOut.checkState() == QtCore.Qt.Checked obj.TPI = self.form.threadTPI.value() - self.updateToolController(obj, self.form.toolController) + try: + self.updateToolController(obj, self.form.toolController) + except PathUtils.PathNoTCExistsException: + title = translate("CAM", "No valid toolcontroller") + message = translate( + "CAM", + "This operation requires a tool controller with a threadmilling tool", + ) + + self.show_error_message(title, message) def setFields(self, obj): """setFields(obj) ... update UI with obj properties' values""" diff --git a/src/Mod/CAM/Path/Op/Gui/Vcarve.py b/src/Mod/CAM/Path/Op/Gui/Vcarve.py index ef6e8818d1..662f32d774 100644 --- a/src/Mod/CAM/Path/Op/Gui/Vcarve.py +++ b/src/Mod/CAM/Path/Op/Gui/Vcarve.py @@ -170,13 +170,23 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): obj.FinishingPass = self.form.finishingPassEnabled.isChecked() if obj.OptimizeMovements != self.form.optimizeMovementsEnabled.isChecked(): - obj.OptimizeMovements = self.form.optimizeMovementsEnabled.isChecked() + obj.OptimizeMovements = self.form.optimizeMovementsEnabled.isChecked() self.finishingPassZOffsetSpinBox.updateProperty() - self.updateToolController(obj, self.form.toolController) self.updateCoolant(obj, self.form.coolantController) + try: + self.updateToolController(obj, self.form.toolController) + except PathUtils.PathNoTCExistsException: + title = translate("CAM", "No valid toolcontroller") + message = translate( + "CAM", + "This operation requires a tool controller with a v-bit tool", + ) + + self.show_error_message(title, message) + def setFields(self, obj): """setFields(obj) ... transfers obj's property values to UI""" self.form.discretize.setValue(obj.Discretize) @@ -201,7 +211,6 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): signals.append(self.form.optimizeMovementsEnabled.stateChanged) - signals.append(self.form.toolController.currentIndexChanged) signals.append(self.form.coolantController.currentIndexChanged) return signals diff --git a/src/Mod/CAM/Path/Op/PocketBase.py b/src/Mod/CAM/Path/Op/PocketBase.py index e22ef75c89..681cdcd936 100644 --- a/src/Mod/CAM/Path/Op/PocketBase.py +++ b/src/Mod/CAM/Path/Op/PocketBase.py @@ -104,6 +104,11 @@ class ObjectPocket(PathAreaOp.ObjectOp): Can safely be overwritten by subclass.""" pass + def opExecute(self, obj): + if len(obj.Base) == 0: + return + super().opExecute(obj) + def areaOpSetDefaultValues(self, obj, job): obj.PocketLastStepOver = 0 diff --git a/src/Mod/CAM/Path/Op/Probe.py b/src/Mod/CAM/Path/Op/Probe.py index 11d4b44e5f..05536327ff 100644 --- a/src/Mod/CAM/Path/Op/Probe.py +++ b/src/Mod/CAM/Path/Op/Probe.py @@ -95,6 +95,10 @@ class ObjectProbing(PathOp.ObjectOp): def opExecute(self, obj): """opExecute(obj) ... generate probe locations.""" Path.Log.track() + if not self.isToolSupported(obj, self.tool): + Path.Log.warning("No suitable probe tool found") + return + self.commandlist.append(Path.Command("(Begin Probing)")) stock = PathUtils.findParentJob(obj).Stock @@ -132,6 +136,11 @@ class ObjectProbing(PathOp.ObjectOp): def opSetDefaultValues(self, obj, job): """opSetDefaultValues(obj, job) ... set default value for RetractHeight""" + def isToolSupported(self, obj, tool): + """Probe operation requires a probe tool""" + support = hasattr(tool, "ShapeName") and (tool.ShapeName == "probe") + Path.Log.track(tool.Label, support) + return support def SetupProperties(): setup = ["Xoffset", "Yoffset", "PointCountX", "PointCountY", "OutputFileName"] diff --git a/src/Mod/CAM/Path/Op/Slot.py b/src/Mod/CAM/Path/Op/Slot.py index b1585c45b2..10d879748a 100644 --- a/src/Mod/CAM/Path/Op/Slot.py +++ b/src/Mod/CAM/Path/Op/Slot.py @@ -601,7 +601,7 @@ class ObjectSlot(PathOp.ObjectOp): if not hasattr(obj, "Base"): msg = translate("CAM_Slot", "No Base Geometry object in the operation.") - FreeCAD.Console.PrintError(msg + "\n") + FreeCAD.Console.PrintUserWarning(msg + "\n") return False if not obj.Base: @@ -609,15 +609,15 @@ class ObjectSlot(PathOp.ObjectOp): p1 = obj.CustomPoint1 p2 = obj.CustomPoint2 if p1 == p2: - msg = translate("CAM_Slot", "Custom points are identical.") - FreeCAD.Console.PrintError(msg + "\n") + msg = translate("CAM_Slot", "Custom points are identical. No slot path will be generated") + FreeCAD.Console.PrintUserWarning(msg + "\n") return False elif p1.z == p2.z: pnts = (p1, p2) featureCount = 2 else: - msg = translate("CAM_Slot", "Custom points not at same Z height.") - FreeCAD.Console.PrintError(msg + "\n") + msg = translate("CAM_Slot", "Custom points not at same Z height. No slot path will be generated") + FreeCAD.Console.PrintUserWarning(msg + "\n") return False else: baseGeom = obj.Base[0] diff --git a/src/Mod/CAM/Path/Op/Vcarve.py b/src/Mod/CAM/Path/Op/Vcarve.py index f1b1230002..8a32139775 100644 --- a/src/Mod/CAM/Path/Op/Vcarve.py +++ b/src/Mod/CAM/Path/Op/Vcarve.py @@ -451,7 +451,7 @@ class ObjectVcarve(PathEngraveBase.ObjectOp): if not currentPosition: return False - + # get vertex position on X/Y plane only v0 = FreeCAD.Base.Vector(currentPosition.x, currentPosition.y) v1 = FreeCAD.Base.Vector(newPosition.x, newPosition.y) @@ -537,16 +537,20 @@ class ObjectVcarve(PathEngraveBase.ObjectOp): self.voronoiDebugCache = None + if obj.ToolController is None: + return + if not hasattr(obj.ToolController.Tool, "CuttingEdgeAngle"): - Path.Log.error( + Path.Log.info( translate( "CAM_Vcarve", "VCarve requires an engraving cutter with a cutting edge angle", ) ) + return if obj.ToolController.Tool.CuttingEdgeAngle >= 180.0: - Path.Log.error( + Path.Log.info( translate( "CAM_Vcarve", "Engraver cutting edge angle must be < 180 degrees." ) @@ -583,7 +587,7 @@ class ObjectVcarve(PathEngraveBase.ObjectOp): ) except Exception as e: - Path.Log.error( + Path.Log.warning( "Error processing Base object. Engraving operation will produce no output." ) import traceback