From 341320908918a37156e0b5e6b1b62ae62e7d7fe2 Mon Sep 17 00:00:00 2001 From: sliptonic Date: Wed, 7 Oct 2020 21:28:17 -0500 Subject: [PATCH 1/3] make deburr check for tool attributes fixes #4327 --- src/Mod/Path/PathScripts/PathDeburr.py | 72 ++++++++++++++++---------- 1 file changed, 45 insertions(+), 27 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathDeburr.py b/src/Mod/Path/PathScripts/PathDeburr.py index 0e45c72658..6d613e5ae6 100644 --- a/src/Mod/Path/PathScripts/PathDeburr.py +++ b/src/Mod/Path/PathScripts/PathDeburr.py @@ -30,7 +30,7 @@ import PathScripts.PathOp as PathOp import PathScripts.PathOpTools as PathOpTools import math -from PySide import QtCore +from PySide import QtCore, QtGui # lazily loaded modules from lazy_loader.lazy_loader import LazyLoader @@ -42,7 +42,7 @@ __url__ = "http://www.freecadweb.org" __doc__ = "Deburr operation." PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) -#PathLog.trackModule(PathLog.thisModule()) +# PathLog.trackModule(PathLog.thisModule()) # Qt translation handling @@ -51,7 +51,14 @@ def translate(context, text, disambig=None): def toolDepthAndOffset(width, extraDepth, tool): - '''toolDepthAndOffset(width, extraDepth, tool) ... return tuple for given parameters.''' + '''toolDepthAndOffset(width, extraDepth, tool) ... return tuple for given\n + parameters.''' + + if not (hasattr(tool, 'CuttingEdgeAngle') + and hasattr(tool, 'FlatRadius') + and hasattr(tool, 'Diameter')): + raise ValueError('Deburr requires tool with flatradius, diameter, and CuttingEdgeAngle\n') + angle = float(tool.CuttingEdgeAngle) if 0 == angle: angle = 180 @@ -62,7 +69,7 @@ def toolDepthAndOffset(width, extraDepth, tool): toolOffset = float(tool.FlatRadius) extraOffset = float(tool.Diameter) / 2 - width if 180 == angle else extraDepth / tan offset = toolOffset + extraOffset - + return (depth, offset) @@ -70,32 +77,44 @@ class ObjectDeburr(PathEngraveBase.ObjectOp): '''Proxy class for Deburr operation.''' def opFeatures(self, obj): - return PathOp.FeatureTool | PathOp.FeatureHeights | PathOp.FeatureStepDown | PathOp.FeatureBaseEdges | PathOp.FeatureBaseFaces | PathOp.FeatureCoolant + return PathOp.FeatureTool | PathOp.FeatureHeights | PathOp.FeatureStepDown | PathOp.FeatureBaseEdges | PathOp.FeatureBaseFaces | PathOp.FeatureCoolant def initOperation(self, obj): PathLog.track(obj.Label) - obj.addProperty('App::PropertyDistance', 'Width', 'Deburr', QtCore.QT_TRANSLATE_NOOP('PathDeburr', 'The desired width of the chamfer')) - obj.addProperty('App::PropertyDistance', 'ExtraDepth', 'Deburr', QtCore.QT_TRANSLATE_NOOP('PathDeburr', 'The additional depth of the tool path')) - obj.addProperty('App::PropertyEnumeration', 'Join', 'Deburr', QtCore.QT_TRANSLATE_NOOP('PathDeburr', 'How to join chamfer segments')) + obj.addProperty('App::PropertyDistance', 'Width', 'Deburr', + QtCore.QT_TRANSLATE_NOOP('PathDeburr', 'The desired width of the chamfer')) + obj.addProperty('App::PropertyDistance', 'ExtraDepth', 'Deburr', + QtCore.QT_TRANSLATE_NOOP('PathDeburr', 'The additional depth of the tool path')) + obj.addProperty('App::PropertyEnumeration', 'Join', 'Deburr', + QtCore.QT_TRANSLATE_NOOP('PathDeburr', 'How to join chamfer segments')) obj.Join = ['Round', 'Miter'] obj.setEditorMode('Join', 2) # hide for now - obj.addProperty('App::PropertyEnumeration', 'Direction', 'Deburr', QtCore.QT_TRANSLATE_NOOP('PathDeburr', 'Direction of Operation')) + obj.addProperty('App::PropertyEnumeration', 'Direction', 'Deburr', + QtCore.QT_TRANSLATE_NOOP('PathDeburr', 'Direction of Operation')) obj.Direction = ['CW', 'CCW'] - obj.addProperty('App::PropertyEnumeration', 'Side', 'Deburr', QtCore.QT_TRANSLATE_NOOP('PathDeburr', 'Side of Operation')) + obj.addProperty('App::PropertyEnumeration', 'Side', 'Deburr', + QtCore.QT_TRANSLATE_NOOP('PathDeburr', 'Side of Operation')) obj.Side = ['Outside', 'Inside'] - obj.setEditorMode('Side', 2) # Hide property, it's calculated by op - obj.addProperty('App::PropertyInteger', 'EntryPoint', 'Deburr', QtCore.QT_TRANSLATE_NOOP('PathDeburr', 'Select the segment, there the operations starts')) + obj.setEditorMode('Side', 2) # Hide property, it's calculated by op + obj.addProperty('App::PropertyInteger', 'EntryPoint', 'Deburr', + QtCore.QT_TRANSLATE_NOOP('PathDeburr', 'Select the segment, there the operations starts')) def opOnDocumentRestored(self, obj): obj.setEditorMode('Join', 2) # hide for now def opExecute(self, obj): PathLog.track(obj.Label) - (depth, offset) = toolDepthAndOffset(obj.Width.Value, obj.ExtraDepth.Value, self.tool) + try: + (depth, offset) = toolDepthAndOffset(obj.Width.Value, obj.ExtraDepth.Value, self.tool) + except ValueError as e: + msg = "{} \n No path will be generated".format(e) + QtGui.QMessageBox.information(None, "Tool Error", msg) + return + PathLog.track(obj.Label, depth, offset) - self.basewires = [] # pylint: disable=attribute-defined-outside-init - self.adjusted_basewires = [] # pylint: disable=attribute-defined-outside-init + self.basewires = [] # pylint: disable=attribute-defined-outside-init + self.adjusted_basewires = [] # pylint: disable=attribute-defined-outside-init wires = [] for base, subs in obj.Base: edges = [] @@ -108,29 +127,29 @@ class ObjectDeburr(PathEngraveBase.ObjectOp): basewires.extend(sub.Wires) else: basewires.append(Part.Wire(sub.Edges)) - self.edges = edges # pylint: disable=attribute-defined-outside-init + self.edges = edges # pylint: disable=attribute-defined-outside-init for edgelist in Part.sortEdges(edges): basewires.append(Part.Wire(edgelist)) self.basewires.extend(basewires) - + # Set default value side = ["Outside"] - + for w in basewires: self.adjusted_basewires.append(w) wire = PathOpTools.offsetWire(w, base.Shape, offset, True, side) if wire: wires.append(wire) - + # Save Outside or Inside obj.Side = side[0] - + # Set direction of op forward = True if obj.Direction == 'CCW': forward = False - + zValues = [] z = 0 if obj.StepDown.Value != 0: @@ -139,13 +158,12 @@ class ObjectDeburr(PathEngraveBase.ObjectOp): zValues.append(z) zValues.append(depth) PathLog.track(obj.Label, depth, zValues) - + if obj.EntryPoint < 0: - obj.EntryPoint = 0; - - self.wires = wires # pylint: disable=attribute-defined-outside-init + obj.EntryPoint = 0 + + self.wires = wires # pylint: disable=attribute-defined-outside-init self.buildpathocc(obj, wires, zValues, True, forward, obj.EntryPoint) - def opRejectAddBase(self, obj, base, sub): '''The chamfer op can only deal with features of the base model, all others are rejected.''' @@ -160,7 +178,7 @@ class ObjectDeburr(PathEngraveBase.ObjectOp): obj.StepDown = '0 mm' obj.Direction = 'CW' obj.Side = "Outside" - obj.EntryPoint = 0; + obj.EntryPoint = 0 def SetupProperties(): From 351267fbfb7c1fc9018da7fd2b955ec5e9844e68 Mon Sep 17 00:00:00 2001 From: sliptonic Date: Wed, 7 Oct 2020 22:50:00 -0500 Subject: [PATCH 2/3] make deburr ignore non-vertical faces fixes #4327 --- src/Mod/Path/PathScripts/PathDeburrGui.py | 34 +++++++++++++++++++---- src/Mod/Path/PathScripts/PathSelection.py | 12 ++++++-- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathDeburrGui.py b/src/Mod/Path/PathScripts/PathDeburrGui.py index 6334b6e9fc..18bb832a3f 100644 --- a/src/Mod/Path/PathScripts/PathDeburrGui.py +++ b/src/Mod/Path/PathScripts/PathDeburrGui.py @@ -28,7 +28,7 @@ import PathScripts.PathDeburr as PathDeburr import PathScripts.PathGui as PathGui import PathScripts.PathLog as PathLog import PathScripts.PathOpGui as PathOpGui - +import Part from PySide import QtCore, QtGui __title__ = "Path Deburr Operation UI" @@ -44,10 +44,30 @@ if LOGLEVEL: else: PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) + def translate(context, text, disambig=None): return QtCore.QCoreApplication.translate(context, text, disambig) +class TaskPanelBaseGeometryPage(PathOpGui.TaskPanelBaseGeometryPage): + '''Enhanced base geometry page to also allow special base objects.''' + + def super(self): + return super(TaskPanelBaseGeometryPage, self) + + def addBaseGeometry(self, selection): + for sel in selection: + if sel.HasSubObjects: + # selectively add some elements of the drawing to the Base + for sub in sel.SubObjects: + if isinstance(sub, Part.Face): + if sub.normalAt(0, 0) != FreeCAD.Vector(0, 0, 1): + PathLog.info(translate("Path", "Ignoring non-vertical Face")) + return + + self.super().addBaseGeometry(selection) + + class TaskPanelOpPage(PathOpGui.TaskPanelPage): '''Page controller class for the Deburr operation.''' @@ -55,8 +75,8 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): return FreeCADGui.PySideUic.loadUi(":/panels/PageOpDeburrEdit.ui") def initPage(self, obj): - self.opImagePath = "{}Mod/Path/Images/Ops/{}".format(FreeCAD.getHomePath(), 'chamfer.svg') # pylint: disable=attribute-defined-outside-init - self.opImage = QtGui.QPixmap(self.opImagePath) # pylint: disable=attribute-defined-outside-init + self.opImagePath = "{}Mod/Path/Images/Ops/{}".format(FreeCAD.getHomePath(), 'chamfer.svg') # pylint: disable=attribute-defined-outside-init + self.opImage = QtGui.QPixmap(self.opImagePath) # pylint: disable=attribute-defined-outside-init self.form.opImage.setPixmap(self.opImage) iconMiter = QtGui.QIcon(':/icons/edge-join-miter-not.svg') iconMiter.addFile(':/icons/edge-join-miter.svg', state=QtGui.QIcon.On) @@ -72,7 +92,7 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): obj.Join = 'Round' elif self.form.joinMiter.isChecked(): obj.Join = 'Miter' - + if obj.Direction != str(self.form.direction.currentText()): obj.Direction = str(self.form.direction.currentText()) @@ -109,6 +129,11 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): self.form.value_W.editingFinished.connect(self.updateWidth) self.form.value_h.editingFinished.connect(self.updateExtraDepth) + def taskPanelBaseGeometryPage(self, obj, features): + '''taskPanelBaseGeometryPage(obj, features) ... return page for adding base geometries.''' + print(features) + return TaskPanelBaseGeometryPage(obj, features) + Command = PathOpGui.SetupOperation('Deburr', PathDeburr.Create, @@ -119,4 +144,3 @@ Command = PathOpGui.SetupOperation('Deburr', PathDeburr.SetupProperties) FreeCAD.Console.PrintLog("Loading PathDeburrGui... done\n") - diff --git a/src/Mod/Path/PathScripts/PathSelection.py b/src/Mod/Path/PathScripts/PathSelection.py index f6483f8505..91e899a4ac 100644 --- a/src/Mod/Path/PathScripts/PathSelection.py +++ b/src/Mod/Path/PathScripts/PathSelection.py @@ -107,15 +107,21 @@ class CHAMFERGate(PathBaseGate): if math.fabs(shape.Volume) < 1e-9 and len(shape.Wires) > 0: return True - if 'Edge' == shape.ShapeType or 'Face' == shape.ShapeType: + if shape.ShapeType == 'Edge': return True + if (shape.ShapeType == 'Face' + and shape.normalAt(0,0) == FreeCAD.Vector(0,0,1)): + return True + if sub: subShape = shape.getElement(sub) - if 'Edge' == subShape.ShapeType or 'Face' == subShape.ShapeType: + if subShape.ShapeType == 'Edge': + return True + elif (subShape.ShapeType == 'Face' + and subShape.normalAt(0,0) == FreeCAD.Vector(0,0,1)): return True - print(shape.ShapeType) return False From a785c0e0a1a1817f4b3f2b7957039af685e5546d Mon Sep 17 00:00:00 2001 From: sliptonic Date: Sat, 10 Oct 2020 08:58:43 -0500 Subject: [PATCH 3/3] Fix deburr CW/CCW calculation --- src/Mod/Path/PathScripts/PathDeburr.py | 43 ++++++++++++--------- src/Mod/Path/PathScripts/PathDeburrGui.py | 3 +- src/Mod/Path/PathScripts/PathEngraveBase.py | 9 ++++- src/Mod/Path/PathScripts/PathOpTools.py | 11 +++--- 4 files changed, 38 insertions(+), 28 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathDeburr.py b/src/Mod/Path/PathScripts/PathDeburr.py index 6d613e5ae6..b60fda27da 100644 --- a/src/Mod/Path/PathScripts/PathDeburr.py +++ b/src/Mod/Path/PathScripts/PathDeburr.py @@ -30,7 +30,7 @@ import PathScripts.PathOp as PathOp import PathScripts.PathOpTools as PathOpTools import math -from PySide import QtCore, QtGui +from PySide import QtCore # lazily loaded modules from lazy_loader.lazy_loader import LazyLoader @@ -54,20 +54,28 @@ def toolDepthAndOffset(width, extraDepth, tool): '''toolDepthAndOffset(width, extraDepth, tool) ... return tuple for given\n parameters.''' - if not (hasattr(tool, 'CuttingEdgeAngle') - and hasattr(tool, 'FlatRadius') - and hasattr(tool, 'Diameter')): - raise ValueError('Deburr requires tool with flatradius, diameter, and CuttingEdgeAngle\n') + if not hasattr(tool, 'Diameter'): + raise ValueError('Deburr requires tool with diameter\n') - angle = float(tool.CuttingEdgeAngle) - if 0 == angle: + if not hasattr(tool, 'CuttingEdgeAngle'): + angle = 180 + FreeCAD.Console.PrintMessage('The selected tool has No CuttingEdgeAngle property. Assuming Endmill\n') + else: + angle = float(tool.CuttingEdgeAngle) + + if not hasattr(tool, 'FlatRadius'): + toolOffset = float(tool.Diameter / 2) + FreeCAD.Console.PrintMessage('The selected tool has no FlatRadius property. Using Diameter\n') + else: + toolOffset = float(tool.FlatRadius) + + if angle == 0: angle = 180 tan = math.tan(math.radians(angle / 2)) toolDepth = 0 if 0 == tan else width / tan depth = toolDepth + extraDepth - toolOffset = float(tool.FlatRadius) - extraOffset = float(tool.Diameter) / 2 - width if 180 == angle else extraDepth / tan + extraOffset = float(tool.Diameter) / 2 - width if angle == 180 else extraDepth / tan offset = toolOffset + extraOffset return (depth, offset) @@ -108,8 +116,9 @@ class ObjectDeburr(PathEngraveBase.ObjectOp): (depth, offset) = toolDepthAndOffset(obj.Width.Value, obj.ExtraDepth.Value, self.tool) except ValueError as e: msg = "{} \n No path will be generated".format(e) - QtGui.QMessageBox.information(None, "Tool Error", msg) - return + raise ValueError(msg) + # QtGui.QMessageBox.information(None, "Tool Error", msg) + # return PathLog.track(obj.Label, depth, offset) @@ -133,22 +142,18 @@ class ObjectDeburr(PathEngraveBase.ObjectOp): self.basewires.extend(basewires) - # Set default value - side = ["Outside"] for w in basewires: self.adjusted_basewires.append(w) - wire = PathOpTools.offsetWire(w, base.Shape, offset, True, side) + wire = PathOpTools.offsetWire(w, base.Shape, offset, True) #, obj.Side) if wire: wires.append(wire) - # Save Outside or Inside - obj.Side = side[0] + # # Save Outside or Inside + # obj.Side = side[0] # Set direction of op - forward = True - if obj.Direction == 'CCW': - forward = False + forward = (obj.Direction == 'CCW') zValues = [] z = 0 diff --git a/src/Mod/Path/PathScripts/PathDeburrGui.py b/src/Mod/Path/PathScripts/PathDeburrGui.py index 18bb832a3f..1d84135d07 100644 --- a/src/Mod/Path/PathScripts/PathDeburrGui.py +++ b/src/Mod/Path/PathScripts/PathDeburrGui.py @@ -62,7 +62,7 @@ class TaskPanelBaseGeometryPage(PathOpGui.TaskPanelBaseGeometryPage): for sub in sel.SubObjects: if isinstance(sub, Part.Face): if sub.normalAt(0, 0) != FreeCAD.Vector(0, 0, 1): - PathLog.info(translate("Path", "Ignoring non-vertical Face")) + PathLog.info(translate("Path", "Ignoring non-horizontal Face")) return self.super().addBaseGeometry(selection) @@ -131,7 +131,6 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): def taskPanelBaseGeometryPage(self, obj, features): '''taskPanelBaseGeometryPage(obj, features) ... return page for adding base geometries.''' - print(features) return TaskPanelBaseGeometryPage(obj, features) diff --git a/src/Mod/Path/PathScripts/PathEngraveBase.py b/src/Mod/Path/PathScripts/PathEngraveBase.py index 7606ec9b58..dc8689aa91 100644 --- a/src/Mod/Path/PathScripts/PathEngraveBase.py +++ b/src/Mod/Path/PathScripts/PathEngraveBase.py @@ -77,16 +77,20 @@ class ObjectOp(PathOp.ObjectOp): last = None for z in zValues: + PathLog.debug(z) if last: self.appendCommand(Path.Command('G1', {'X': last.x, 'Y': last.y, 'Z': last.z}), z, relZ, self.vertFeed) first = True if start_idx > len(edges)-1: start_idx = len(edges)-1 - + edges = edges[start_idx:] + edges[:start_idx] for edge in edges: + PathLog.debug("points: {} -> {}".format(edge.Vertexes[0].Point, edge.Vertexes[-1].Point)) + PathLog.debug("valueat {} -> {}".format(edge.valueAt(edge.FirstParameter), edge.valueAt(edge.LastParameter))) if first and (not last or not wire.isClosed()): + PathLog.debug('processing first edge entry') # we set the first move to our first point last = edge.Vertexes[0].Point @@ -96,7 +100,8 @@ class ObjectOp(PathOp.ObjectOp): self.appendCommand(Path.Command('G1', {'X': last.x, 'Y': last.y, 'Z': last.z}), z, relZ, self.vertFeed) first = False - if PathGeom.pointsCoincide(last, edge.Vertexes[0].Point): + if PathGeom.pointsCoincide(last, edge.valueAt(edge.FirstParameter)): + #if PathGeom.pointsCoincide(last, edge.Vertexes[0].Point): for cmd in PathGeom.cmdsForEdge(edge): self.appendCommand(cmd, z, relZ, self.horizFeed) last = edge.Vertexes[-1].Point diff --git a/src/Mod/Path/PathScripts/PathOpTools.py b/src/Mod/Path/PathScripts/PathOpTools.py index 40e7105ca5..c7c4baf540 100644 --- a/src/Mod/Path/PathScripts/PathOpTools.py +++ b/src/Mod/Path/PathScripts/PathOpTools.py @@ -136,6 +136,7 @@ def orientWire(w, forward=True): If forward = True (the default) the wire is oriented clockwise, looking down the negative Z axis. If forward = False the wire is oriented counter clockwise. If forward = None the orientation is determined by the order in which the edges appear in the wire.''' + PathLog.debug('orienting forward: {}'.format(forward)) wire = Part.Wire(_orientEdges(w.Edges)) if forward is not None: if forward != _isWireClockwise(wire): @@ -144,7 +145,7 @@ def orientWire(w, forward=True): PathLog.track('orientWire - ok') return wire -def offsetWire(wire, base, offset, forward, Side = None): +def offsetWire(wire, base, offset, forward):#, Side = None): '''offsetWire(wire, base, offset, forward) ... offsets the wire away from base and orients the wire accordingly. The function tries to avoid most of the pitfalls of Part.makeOffset2D which is possible because all offsetting happens in the XY plane. @@ -198,12 +199,12 @@ def offsetWire(wire, base, offset, forward, Side = None): if wire.isClosed(): if not base.isInside(owire.Edges[0].Vertexes[0].Point, offset/2, True): PathLog.track('closed - outside') - if Side: - Side[0] = "Outside" + # if Side: + # Side[0] = "Outside" return orientWire(owire, forward) PathLog.track('closed - inside') - if Side: - Side[0] = "Inside" + # if Side: + # Side[0] = "Inside" try: owire = wire.makeOffset2D(-offset) except Exception: # pylint: disable=broad-except