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
This commit is contained in:
sliptonic
2024-06-15 09:30:53 -05:00
parent c9db224412
commit cb23383526
15 changed files with 134 additions and 54 deletions

View File

@@ -1639,7 +1639,9 @@ std::vector<shared_ptr<Area> > 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) {

View File

@@ -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),

View File

@@ -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)

View File

@@ -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):

View File

@@ -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)
)

View File

@@ -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:

View File

@@ -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."""

View File

@@ -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):

View File

@@ -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)

View File

@@ -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"""

View File

@@ -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

View File

@@ -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

View File

@@ -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"]

View File

@@ -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]

View File

@@ -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