From b5f37f06aae8db98437136cfe36452d38585af76 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 ed61873b2c67fbb90c27d3c3166f7dbb4b7ebf48 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 ffd7693815a13f41ad4f5bb8e2f12f88a2213c61 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