Merge pull request #3132 from Schildkroet/deburr+dressup

[Path] Deburr+DressupLeadInOut fixes
This commit is contained in:
sliptonic
2020-04-05 18:34:52 -05:00
committed by GitHub
5 changed files with 273 additions and 78 deletions

View File

@@ -3,6 +3,7 @@
# ***************************************************************************
# * *
# * Copyright (c) 2018 sliptonic <shopinthewoods@gmail.com> *
# * 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) *
@@ -58,6 +59,8 @@ def toolDepthAndOffset(width, extraDepth, tool):
toolOffset = float(tool.FlatRadius)
extraOffset = float(tool.Diameter) / 2 - width if 180 == angle else extraDepth / tan
offset = toolOffset + extraOffset
if offset < 0.0001:
offset = 0.01
return (depth, offset)
@@ -76,6 +79,10 @@ class ObjectDeburr(PathEngraveBase.ObjectOp):
obj.setEditorMode('Join', 2) # hide for now
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.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'))
def opOnDocumentRestored(self, obj):
obj.setEditorMode('Join', 2) # hide for now
@@ -104,13 +111,20 @@ class ObjectDeburr(PathEngraveBase.ObjectOp):
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)
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
@@ -123,9 +137,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
self.buildpathocc(obj, wires, zValues, True, forward)
self.buildpathocc(obj, wires, zValues, True, forward, obj.EntryPoint)
# the last command is a move to clearance, which is automatically added by PathOp
if self.commandlist:
@@ -138,11 +155,13 @@ class ObjectDeburr(PathEngraveBase.ObjectOp):
def opSetDefaultValues(self, obj, job):
PathLog.track(obj.Label, job.Label)
obj.Width = '1 mm'
obj.ExtraDepth = '0.1 mm'
obj.ExtraDepth = '0.5 mm'
obj.Join = 'Round'
obj.setExpression('StepDown', '0 mm')
obj.StepDown = '0 mm'
obj.Direction = 'CW'
obj.Side = "Outside"
obj.EntryPoint = 0;
def SetupProperties():

View File

@@ -3,6 +3,7 @@
# ***************************************************************************
# * *
# * Copyright (c) 2017 LTS <SammelLothar@gmx.de> under LGPL *
# * 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) *
@@ -31,6 +32,7 @@ import PathScripts.PathGeom as PathGeom
import PathScripts.PathLog as PathLog
import PathScripts.PathUtils as PathUtils
import math
import copy
from PySide import QtCore
@@ -68,7 +70,11 @@ class ObjectDressup:
obj.addProperty("App::PropertyEnumeration", "RadiusCenter", "Path", QtCore.QT_TRANSLATE_NOOP("Path_DressupLeadInOut", "The Mode of Point Radiusoffset or Center"))
obj.RadiusCenter = ["Radius", "Center"]
obj.Proxy = self
obj.addProperty("App::PropertyDistance", "ExtendLeadIn", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property", "Extends LeadIn distance"))
obj.addProperty("App::PropertyDistance", "ExtendLeadOut", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property", "Extends LeadOut distance"))
obj.addProperty("App::PropertyBool", "RapidPlunge", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property", "Perform plunges with G0"))
obj.addProperty("App::PropertyBool", "IncludeLayers", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property", "Apply LeadInOut to layers within an operation"))
self.wire = None
self.rapids = None
@@ -80,7 +86,7 @@ class ObjectDressup:
return None
def setup(self, obj):
obj.Length = 5.0
obj.Length = obj.Base.ToolController.Tool.Diameter * 0.75
obj.LeadIn = True
obj.LeadOut = True
obj.KeepToolDown = False
@@ -88,6 +94,10 @@ class ObjectDressup:
obj.StyleOn = 'Arc'
obj.StyleOff = 'Arc'
obj.RadiusCenter = 'Radius'
obj.ExtendLeadIn = 0
obj.ExtendLeadOut = 0
obj.RapidPlunge = False
obj.IncludeLayers = True
def execute(self, obj):
if not obj.Base:
@@ -113,78 +123,173 @@ class ObjectDressup:
if hasattr(op, 'Direction') and op.Direction == 'CW':
return 'right'
return 'left'
def getSideOfPath(self, obj):
op = PathDressup.baseOp(obj.Base)
if hasattr(op, 'Side'):
return op.Side
return ''
def normalize(self, Vector):
x = Vector.x
y = Vector.y
length = math.sqrt(x*x + y*y)
if((math.fabs(length)) > 0.0000000000001):
vx = round(x / length, 0)
vy = round(y / length, 0)
vx = round(x / length, 3)
vy = round(y / length, 3)
return FreeCAD.Vector(vx, vy, 0)
def invert(self, Vector):
x = Vector.x * -1
y = Vector.y * -1
z = Vector.z * -1
return FreeCAD.Vector(x, y, z)
def multiply(self, Vector, len):
x = Vector.x * len
y = Vector.y * len
z = Vector.z * len
return FreeCAD.Vector(x, y, z)
def rotate(self, Vector, angle):
s = math.sin(math.radians(angle))
c = math.cos(math.radians(angle))
xnew = Vector.x * c - Vector.y * s;
ynew = Vector.x * s + Vector.y * c;
return FreeCAD.Vector(xnew, ynew, Vector.z)
def getLeadStart(self, obj, queue, action):
'''returns Lead In G-code.'''
results = []
# zdepth = currLocation["Z"]
op = PathDressup.baseOp(obj.Base)
tc = PathDressup.toolController(obj.Base)
horizFeed = tc.HorizFeed.Value
vertFeed = tc.VertFeed.Value
toolnummer = tc.ToolNumber
# set the correct twist command
arcs_identical = False
# Set the correct twist command
if self.getDirectionOfPath(obj) == 'left':
arcdir = "G3"
else:
arcdir = "G2"
R = obj.Length.Value # Radius of roll or length
if queue[1].Name == "G1": # line
p0 = queue[0].Placement.Base
p1 = queue[1].Placement.Base
v = self.normalize(p1.sub(p0))
# PathLog.notice(" CURRENT_IN : P0 Z:{} p1 Z:{}".format(p0.z,p1.z))
# PathLog.debug(" CURRENT_IN : P0 Z:{} p1 Z:{}".format(p0.z,p1.z))
else:
p0 = queue[0].Placement.Base
p1 = queue[1].Placement.Base
# PathLog.notice(" CURRENT_IN ARC : P0 X:{} Y:{} P1 X:{} Y:{} ".format(p0.x,p0.y,p1.x,p1.y))
v = self.normalize(p1.sub(p0))
# PathLog.debug(" CURRENT_IN ARC : P0 X:{} Y:{} P1 X:{} Y:{} ".format(p0.x,p0.y,p1.x,p1.y))
# Calculate offset vector (will be overwritten for arcs)
if self.getDirectionOfPath(obj) == 'right':
off_v = FreeCAD.Vector(v.y*R, -v.x*R, 0.0)
else:
off_v = FreeCAD.Vector(-v.y*R, v.x*R, 0.0)
offsetvector = FreeCAD.Vector(v.x*R, v.y*R, 0) # IJ
# Check if we enter at line or arc command
if queue[1].Name in movecommands and queue[1].Name not in arccommands:
# We have a line move
vec = p1.sub(p0)
vec_n = self.normalize(vec)
vec_inv = self.invert(vec_n)
vec_off = self.multiply(vec_inv, obj.ExtendLeadIn)
#PathLog.debug("LineCMD: {}, Vxinv: {}, Vyinv: {}, Vxoff: {}, Vyoff: {}".format(queue[0].Name, vec_inv.x, vec_inv.y, vec_off.x, vec_off.y))
else:
# We have an arc move
# Calculate coordinates for middle of circle
pij = copy.deepcopy(p0)
pij.x += queue[1].Parameters['I']
pij.y += queue[1].Parameters['J']
# Check if lead in and operation go in same direction (usually for inner circles)
if arcdir == queue[1].Name:
arcs_identical = True
# Calculate vector circle start -> circle middle
vec_circ = pij.sub(p0)
# Rotate vector to get direction for lead in
if arcdir == "G2":
vec_rot = self.rotate(vec_circ, 90)
else:
vec_rot = self.rotate(vec_circ, -90)
# Normalize and invert vector
vec_n = self.normalize(vec_rot)
v = self.invert(vec_n)
# Calculate offset of lead in
if arcdir == "G3":
off_v = FreeCAD.Vector(-v.y*R, v.x*R, 0.0)
else:
off_v = FreeCAD.Vector(v.y*R, -v.x*R, 0.0)
# Multiply offset by LeadIn length
vec_off = self.multiply(vec_n, obj.ExtendLeadIn)
offsetvector = FreeCAD.Vector(v.x*R-vec_off.x, v.y*R-vec_off.y, 0) # IJ
if obj.RadiusCenter == 'Radius':
leadstart = (p0.add(off_v)).sub(offsetvector) # Rmode
if arcs_identical:
t = p0.sub(leadstart)
t = p0.add(t)
leadstart = t
offsetvector = self.multiply(offsetvector, -1)
else:
leadstart = p0.add(off_v) # Dmode
if action == 'start':
extendcommand = Path.Command('G0', {"X": 0.0, "Y": 0.0, "Z": op.ClearanceHeight.Value})
results.append(extendcommand)
#extendcommand = Path.Command('G0', {"X": 0.0, "Y": 0.0, "Z": op.ClearanceHeight.Value})
#results.append(extendcommand)
extendcommand = Path.Command('G0', {"X": leadstart.x, "Y": leadstart.y, "Z": op.ClearanceHeight.Value})
results.append(extendcommand)
extendcommand = Path.Command('G0', {"X": leadstart.x, "Y": leadstart.y, "Z": op.SafeHeight.Value})
extendcommand = Path.Command('G0', {"Z": op.SafeHeight.Value})
results.append(extendcommand)
if action == 'layer':
if not obj.KeepToolDown:
extendcommand = Path.Command('G0', {"Z": op.SafeHeight.Value})
results.append(extendcommand)
extendcommand = Path.Command('G0', {"X": leadstart.x, "Y": leadstart.y})
results.append(extendcommand)
extendcommand = Path.Command('G1', {"X": leadstart.x, "Y": leadstart.y, "Z": p1.z, "F": vertFeed})
if not obj.RapidPlunge:
extendcommand = Path.Command('G1', {"X": leadstart.x, "Y": leadstart.y, "Z": p1.z, "F": vertFeed})
else:
extendcommand = Path.Command('G0', {"X": leadstart.x, "Y": leadstart.y, "Z": p1.z,})
results.append(extendcommand)
if obj.UseMachineCRC:
if self.getDirectionOfPath(obj) == 'right':
results.append(Path.Command('G42', {'D': toolnummer}))
else:
results.append(Path.Command('G41', {'D': toolnummer}))
if obj.StyleOn == 'Arc':
arcmove = Path.Command(arcdir, {"X": p0.x, "Y": p0.y, "I": offsetvector.x, "J": offsetvector.y, "F": horizFeed}) # add G2/G3 move
arcmove = Path.Command(arcdir, {"X": p0.x+vec_off.x, "Y": p0.y+vec_off.y, "I": offsetvector.x+vec_off.x, "J": offsetvector.y+vec_off.y, "F": horizFeed}) # add G2/G3 move
results.append(arcmove)
if obj.ExtendLeadIn != 0:
extendcommand = Path.Command('G1', {"X": p0.x, "Y": p0.y, "F": horizFeed})
results.append(extendcommand)
elif obj.StyleOn == 'Tangent':
extendcommand = Path.Command('G1', {"X": p0.x, "Y": p0.y, "F": horizFeed})
results.append(extendcommand)
else:
PathLog.notice(" CURRENT_IN Perp")
PathLog.debug(" CURRENT_IN Perp")
currLocation.update(results[-1].Parameters)
currLocation['Z'] = p1.z
return results
def getLeadEnd(self, obj, queue, action):
@@ -193,11 +298,14 @@ class ObjectDressup:
results = []
horizFeed = PathDressup.toolController(obj.Base).HorizFeed.Value
R = obj.Length.Value # Radius of roll or length
# set the correct twist command
arcs_identical = False
# Set the correct twist command
if self.getDirectionOfPath(obj) == 'right':
arcdir = "G2"
else:
arcdir = "G3"
if queue[1].Name == "G1": # line
p0 = queue[0].Placement.Base
p1 = queue[1].Placement.Base
@@ -206,97 +314,157 @@ class ObjectDressup:
p0 = queue[0].Placement.Base
p1 = queue[1].Placement.Base
v = self.normalize(p1.sub(p0))
if self.getDirectionOfPath(obj) == 'right':
off_v = FreeCAD.Vector(v.y*R, -v.x*R, 0.0)
else:
off_v = FreeCAD.Vector(-v.y*R, v.x*R, 0.0)
offsetvector = FreeCAD.Vector(v.x*R, v.y*R, 0.0)
# Check if we leave at line or arc command
if queue[1].Name in movecommands and queue[1].Name not in arccommands:
# We have a line move
vec = p1.sub(p0)
vec_n = self.normalize(vec)
vec_inv = self.invert(vec_n)
vec_off = self.multiply(vec_inv, obj.ExtendLeadOut)
#PathLog.debug("LineCMD: {}, Vxinv: {}, Vyinv: {}, Vxoff: {}, Vyoff: {}".format(queue[0].Name, vec_inv.x, vec_inv.y, vec_off.x, vec_off.y))
else:
# We have an arc move
pij = copy.deepcopy(p0)
pij.x += queue[1].Parameters['I']
pij.y += queue[1].Parameters['J']
ve = pij.sub(p1)
if arcdir == queue[1].Name:
arcs_identical = True
if arcdir == "G2":
vec_rot = self.rotate(ve, -90)
else:
vec_rot = self.rotate(ve, 90)
vec_n = self.normalize(vec_rot)
v = vec_n
if arcdir == "G3":
off_v = FreeCAD.Vector(-v.y*R, v.x*R, 0.0)
else:
off_v = FreeCAD.Vector(v.y*R, -v.x*R, 0.0)
vec_inv = self.invert(vec_rot)
vec_off = self.multiply(vec_inv, obj.ExtendLeadOut)
offsetvector = FreeCAD.Vector(v.x*R-vec_off.x, v.y*R-vec_off.y, 0.0)
if obj.RadiusCenter == 'Radius':
leadend = (p1.add(off_v)).add(offsetvector) # Rmode
if arcs_identical:
t = p1.sub(leadend)
t = p1.add(t)
leadend = t
off_v = self.multiply(off_v, -1)
else:
leadend = p1.add(off_v) # Dmode
IJ = off_v # .negative()
#results.append(queue[1])
if obj.StyleOff == 'Arc':
if obj.ExtendLeadOut != 0:
extendcommand = Path.Command('G1', {"X": p1.x-vec_off.x, "Y": p1.y-vec_off.y, "F": horizFeed})
results.append(extendcommand)
arcmove = Path.Command(arcdir, {"X": leadend.x, "Y": leadend.y, "I": IJ.x, "J": IJ.y, "F": horizFeed}) # add G2/G3 move
results.append(arcmove)
elif obj.StyleOff == 'Tangent':
extendcommand = Path.Command('G1', {"X": leadend.x, "Y": leadend.y, "F": horizFeed})
results.append(extendcommand)
else:
PathLog.notice(" CURRENT_IN Perp")
PathLog.debug(" CURRENT_IN Perp")
if obj.UseMachineCRC: # crc off
results.append(Path.Command('G40', {}))
return results
def generateLeadInOutCurve(self, obj):
global currLocation # pylint: disable=global-statement
firstmove = Path.Command("G0", {"X": 0, "Y": 0, "Z": 0})
op = PathDressup.baseOp(obj.Base)
currLocation.update(firstmove.Parameters)
newpath = []
queue = []
action = 'start'
prevCmd = ''
layers = []
# Read in all commands
for curCommand in obj.Base.Path.Commands:
# replace = None
# don't worry about non-move commands, just add to output
#PathLog.debug("CurCMD: {}".format(curCommand))
if curCommand.Name not in movecommands + rapidcommands:
# Don't worry about non-move commands, just add to output
newpath.append(curCommand)
continue
# rapid retract triggers exit move, else just add to output
if curCommand.Name in rapidcommands:
# detect start position
if (curCommand.x is not None) or (curCommand.y is not None):
firstmove = curCommand
# We don't care about rapid moves
prevCmd = curCommand
currLocation.update(curCommand.Parameters)
if action != 'start': # done move out
if obj.LeadOut:
temp = self.getLeadEnd(obj, queue, 'end')
newpath.extend(temp)
newpath.append(curCommand) # Z clear DONE
continue
if curCommand.Name in movecommands:
if prevCmd.Name in rapidcommands and curCommand.Name in movecommands and len(queue) > 0:
# Layer changed: Save current layer cmds and prepare next layer
layers.append(queue)
queue = []
if obj.IncludeLayers and curCommand.z < currLocation['Z'] and prevCmd.Name in movecommands:
# Layer change within move cmds
#PathLog.debug("Layer change in move: {}->{}".format(currLocation['Z'], curCommand.z))
layers.append(queue)
queue = []
# Save all move commands
queue.append(curCommand)
if action == 'start' and len(queue) < 2:
continue
if action == 'layer':
if len(queue) > 2:
queue.pop(0)
if obj.LeadIn:
temp = self.getLeadStart(obj, queue, action)
newpath.extend(temp)
#newpath.append(curCommand)
action = 'none'
currLocation.update(curCommand.Parameters)
else:
newpath.append(curCommand)
if curCommand.z != currLocation["Z"] and action != 'start': # vertical feeding to depth
if obj.LeadOut: # fish cycle
if len(queue) > 2:
queue.pop(len(queue)-1)
temp = self.getLeadEnd(obj, queue, action)
newpath.extend(temp)
action = 'layer'
if len(queue) > 2:
queue.pop(0)
continue
else:
newpath.append(curCommand)
if len(queue) > 2:
queue.pop(0)
if obj.LeadIn and len(queue) >= 2 and action == 'start':
temp = self.getLeadStart(obj, queue, action)
newpath.extend(temp)
newpath.append(curCommand)
action = 'none'
currLocation.update(curCommand.Parameters)
else:
newpath.append(curCommand)
currLocation.update(curCommand.Parameters)
currLocation.update(curCommand.Parameters)
prevCmd = curCommand
# Add last layer
if len(queue) > 0:
layers.append(queue)
queue = []
# Go through each layer and add leadIn/Out
idx = 0
for layer in layers:
#PathLog.debug("Layer {}".format(idx))
if obj.LeadIn:
temp = self.getLeadStart(obj, layer, action)
newpath.extend(temp)
for cmd in layer:
#PathLog.debug("CurLoc: {}, NewCmd: {}".format(currLocation, cmd))
#if currLocation['X'] == cmd.x and currLocation['Y'] == cmd.y and currLocation['Z'] == cmd.z and cmd.Name in ['G1', 'G01']:
#continue
newpath.append(cmd)
if obj.LeadOut:
tmp = []
tmp.append(layer[-2])
tmp.append(layer[-1])
temp = self.getLeadEnd(obj, tmp, action)
newpath.extend(temp)
if not obj.KeepToolDown or idx == len(layers)-1:
extendcommand = Path.Command('G0', {"Z": op.ClearanceHeight.Value})
newpath.append(extendcommand)
else:
action = 'layer'
idx += 1
commands = newpath
return Path.Path(commands)
class ViewProviderDressup:
def __init__(self, vobj):

View File

@@ -59,7 +59,7 @@ class ObjectOp(PathOp.ObjectOp):
zValues.append(obj.FinalDepth.Value)
return zValues
def buildpathocc(self, obj, wires, zValues, relZ=False, forward=True):
def buildpathocc(self, obj, wires, zValues, relZ=False, forward=True, start_idx=0):
'''buildpathocc(obj, wires, zValues, relZ=False) ... internal helper function to generate engraving commands.'''
PathLog.track(obj.Label, len(wires), zValues)
@@ -78,6 +78,10 @@ class ObjectOp(PathOp.ObjectOp):
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:
if first and (not last or not wire.isClosed()):
# we set the first move to our first point
@@ -86,7 +90,7 @@ class ObjectOp(PathOp.ObjectOp):
self.commandlist.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
self.commandlist.append(Path.Command('G0', {'X': last.x, 'Y': last.y, 'F': self.horizRapid}))
self.commandlist.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
self.appendCommand(Path.Command('G1', {'Z': last.z}), z, relZ, self.vertFeed)
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):

View File

@@ -141,7 +141,7 @@ def orientWire(w, forward=True):
PathLog.track('orientWire - ok')
return wire
def offsetWire(wire, base, offset, forward):
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.
@@ -195,8 +195,12 @@ def offsetWire(wire, base, offset, forward):
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"
return orientWire(owire, forward)
PathLog.track('closed - inside')
if Side:
Side[0] = "Inside"
try:
owire = wire.makeOffset2D(-offset)
except Exception: # pylint: disable=broad-except

View File

@@ -139,7 +139,7 @@ class ObjectProfile(PathProfileBase.ObjectProfile):
if FreeCAD.GuiUp:
import FreeCADGui
FreeCADGui.ActiveDocument.getObject(tmpGrpNm).Visibility = False
return shapes
def _flattenWire(self, obj, wire, trgtDep):