524 lines
20 KiB
Python
524 lines
20 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# ***************************************************************************
|
|
# * *
|
|
# * Copyright (c) 2014 sliptonic <shopinthewoods@gmail.com> *
|
|
# * *
|
|
# * This program is free software; you can redistribute it and/or modify *
|
|
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
|
# * as published by the Free Software Foundation; either version 2 of *
|
|
# * the License, or (at your option) any later version. *
|
|
# * for detail see the LICENCE text file. *
|
|
# * *
|
|
# * This program is distributed in the hope that it will be useful, *
|
|
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
# * GNU Library General Public License for more details. *
|
|
# * *
|
|
# * You should have received a copy of the GNU Library General Public *
|
|
# * License along with this program; if not, write to the Free Software *
|
|
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
|
# * USA *
|
|
# * *
|
|
# ***************************************************************************
|
|
|
|
import FreeCAD
|
|
import FreeCADGui
|
|
import Path
|
|
#import PathGui
|
|
from PySide import QtCore, QtGui
|
|
import math
|
|
import DraftVecUtils as D
|
|
import PathScripts.PathUtils as P
|
|
|
|
"""Dragknife Dressup object and FreeCAD command"""
|
|
|
|
# Qt tanslation handling
|
|
try:
|
|
_encoding = QtGui.QApplication.UnicodeUTF8
|
|
|
|
def translate(context, text, disambig=None):
|
|
return QtGui.QApplication.translate(context, text, disambig, _encoding)
|
|
except AttributeError:
|
|
def translate(context, text, disambig=None):
|
|
return QtGui.QApplication.translate(context, text, disambig)
|
|
|
|
movecommands = ['G1', 'G01', 'G2', 'G02', 'G3', 'G03']
|
|
rapidcommands = ['G0', 'G00']
|
|
arccommands = ['G2', 'G3', 'G02', 'G03']
|
|
|
|
currLocation = {}
|
|
|
|
|
|
class ObjectDressup:
|
|
|
|
def __init__(self, obj):
|
|
obj.addProperty("App::PropertyLink", "Base", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property","The base path to modify"))
|
|
obj.addProperty("App::PropertyAngle", "filterangle", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property","Angles less than filter angle will not receive corner actions"))
|
|
obj.addProperty("App::PropertyFloat", "offset", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property","Distance the point trails behind the spindle"))
|
|
obj.addProperty("App::PropertyFloat", "pivotheight", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property","Height to raise during corner action"))
|
|
|
|
obj.Proxy = self
|
|
|
|
def __getstate__(self):
|
|
return None
|
|
|
|
def __setstate__(self, state):
|
|
return None
|
|
|
|
def shortcut(self, queue):
|
|
'''Determines whether its shorter to twist CW or CCW to align with the next move'''
|
|
# get the vector of the last move
|
|
|
|
global arccommands
|
|
|
|
if queue[1].Name in arccommands:
|
|
arcLoc = FreeCAD.Vector(queue[2].x + queue[1].I, queue[2].y + queue[1].J, currLocation['Z'])
|
|
radvector = arcLoc.sub(queue[1].Placement.Base) #.sub(arcLoc) # vector of chord from center to point
|
|
# vector of line perp to chord.
|
|
v1 = radvector.cross(FreeCAD.Vector(0, 0, 1))
|
|
else:
|
|
v1 = queue[1].Placement.Base.sub(queue[2].Placement.Base)
|
|
|
|
# get the vector of the current move
|
|
if queue[0].Name in arccommands:
|
|
arcLoc = FreeCAD.Vector( (queue[1].x + queue[0].I), (queue[1].y + queue[0].J), currLocation['Z'])
|
|
radvector = queue[1].Placement.Base.sub(arcLoc) # calculate arcangle
|
|
v2 = radvector.cross(FreeCAD.Vector(0, 0, 1))
|
|
else:
|
|
v2 = queue[0].Placement.Base.sub(queue[1].Placement.Base)
|
|
|
|
if (v2.x * v1.y) - (v2.y * v1.x) >= 0:
|
|
return "CW"
|
|
else:
|
|
return "CCW"
|
|
|
|
def segmentAngleXY(self, prevCommand, currCommand, endpos=False, currentZ=0):
|
|
'''returns in the starting angle in radians for a Path command.
|
|
requires the previous command in order to calculate arcs correctly
|
|
if endpos = True, return the angle at the end of the segment.'''
|
|
|
|
global arccommands
|
|
if currCommand.Name in arccommands:
|
|
arcLoc = FreeCAD.Vector( (prevCommand.x + currCommand.I), (prevCommand.y + currCommand.J), currentZ)
|
|
if endpos is True:
|
|
radvector = arcLoc.sub(currCommand.Placement.Base) #Calculate vector at start of arc
|
|
else:
|
|
radvector = arcLoc.sub(prevCommand.Placement.Base) #Calculate vector at end of arc
|
|
|
|
v1 = radvector.cross(FreeCAD.Vector(0, 0, 1))
|
|
if currCommand.Name in ["G2", "G02"]:
|
|
v1 = D.rotate2D(v1, math.radians(180))
|
|
else:
|
|
v1 = currCommand.Placement.Base.sub(prevCommand.Placement.Base) #Straight segments are easy
|
|
|
|
myAngle = D.angle(v1, FreeCAD.Base.Vector(1, 0, 0), FreeCAD.Base.Vector(0, 0, -1))
|
|
return myAngle
|
|
|
|
def getIncidentAngle(self, queue):
|
|
# '''returns in the incident angle in radians between the current and previous moves'''
|
|
|
|
angleatend = float(math.degrees(self.segmentAngleXY(queue[2], queue[1], True)))
|
|
if angleatend < 0:
|
|
angleatend = 360 + angleatend
|
|
angleatstart = float(math.degrees(self.segmentAngleXY(queue[1], queue[0])))
|
|
if angleatstart < 0:
|
|
angleatstart = 360 + angleatstart
|
|
|
|
incident_angle = angleatend-angleatstart
|
|
|
|
return incident_angle
|
|
|
|
def arcExtension(self, obj, queue):
|
|
'''returns gcode for arc extension'''
|
|
global currLocation
|
|
results = []
|
|
|
|
offset = obj.offset
|
|
# Find the center of the old arc
|
|
C = FreeCAD.Base.Vector(queue[2].x + queue[1].I, queue[2].y + queue[1].J, currLocation['Z'])
|
|
|
|
# Find radius of old arc
|
|
R = math.hypot(queue[1].I, queue[1].J)
|
|
|
|
# Find angle subtended by the extension arc
|
|
theta = math.atan2(queue[1].y - C.y, queue[1].x - C.x)
|
|
if queue[1].Name in ["G2", "G02"]:
|
|
theta = theta - offset / R
|
|
else:
|
|
theta = theta + offset / R
|
|
|
|
# XY coordinates of new arc endpoint.
|
|
Bx = C.x + R * math.cos(theta)
|
|
By = C.y + R * math.sin(theta)
|
|
|
|
# endpoint = FreeCAD.Base.Vector(Bx, By, currLocation["Z"])
|
|
startpoint = queue[1].Placement.Base
|
|
offsetvector = C.sub(startpoint)
|
|
|
|
I = offsetvector.x
|
|
J = offsetvector.y
|
|
|
|
extend = Path.Command(queue[1].Name, {"I": I, "J": J, "X": Bx, "Y": By})
|
|
results.append(extend)
|
|
currLocation.update(extend.Parameters)
|
|
|
|
replace = None
|
|
return (results, replace)
|
|
|
|
def arcTwist(self, obj, queue, lastXY, twistCW=False):
|
|
'''returns gcode to do an arc move toward an arc to perform
|
|
a corner action twist. Inclues lifting and plungeing the knife'''
|
|
|
|
global currLocation
|
|
pivotheight = obj.pivotheight
|
|
offset = obj.offset
|
|
results = []
|
|
|
|
# set the correct twist command
|
|
if twistCW is False:
|
|
arcdir = "G3"
|
|
else:
|
|
arcdir = "G2"
|
|
|
|
# move to the pivot heigth
|
|
zdepth = currLocation["Z"]
|
|
retract = Path.Command("G0", {"Z": pivotheight})
|
|
results.append(retract)
|
|
currLocation.update(retract.Parameters)
|
|
|
|
# get the center of the destination arc
|
|
arccenter = FreeCAD.Base.Vector(queue[1].x + queue[0].I, queue[1].y + queue[0].J, currLocation["Z"])
|
|
|
|
# The center of the twist arc is the old line end point.
|
|
C = queue[1].Placement.Base
|
|
|
|
# Find radius of old arc
|
|
R = math.hypot(queue[0].I, queue[0].J)
|
|
|
|
# find angle of original center to startpoint
|
|
v1 = queue[1].Placement.Base.sub(arccenter)
|
|
segAngle = D.angle(v1, FreeCAD.Base.Vector(1, 0, 0), FreeCAD.Base.Vector(0, 0, -1))
|
|
|
|
# Find angle subtended by the offset
|
|
theta = offset / R
|
|
|
|
# add or subtract theta depending on direction
|
|
if queue[1].Name in ["G2", "G02"]:
|
|
newangle = segAngle + theta
|
|
else:
|
|
newangle = segAngle - theta
|
|
|
|
# calculate endpoints
|
|
Bx = arccenter.x + R * math.cos(newangle)
|
|
By = arccenter.y + R * math.sin(newangle)
|
|
endpointvector = FreeCAD.Base.Vector(Bx, By, currLocation['Z'])
|
|
|
|
# calculate IJ offsets of twist arc from current position.
|
|
offsetvector = C.sub(lastXY)
|
|
|
|
# add G2/G3 move
|
|
arcmove = Path.Command(
|
|
arcdir, {"X": endpointvector.x, "Y": endpointvector.y, "I": offsetvector.x, "J": offsetvector.y})
|
|
results.append(arcmove)
|
|
currLocation.update(arcmove.Parameters)
|
|
|
|
# plunge back to depth
|
|
plunge = Path.Command("G1", {"Z": zdepth})
|
|
results.append(plunge)
|
|
currLocation.update(plunge.Parameters)
|
|
|
|
# The old arc move won't work so calculate a replacement command
|
|
offsetv = arccenter.sub(endpointvector)
|
|
|
|
replace = Path.Command(
|
|
queue[0].Name, {"X": queue[0].X, "Y": queue[0].Y, "I": offsetv.x, "J": offsetv.y})
|
|
return (results, replace)
|
|
|
|
def lineExtension(self, obj, queue):
|
|
'''returns gcode for line extension'''
|
|
global currLocation
|
|
|
|
offset = float(obj.offset)
|
|
results = []
|
|
|
|
v1 = queue[1].Placement.Base.sub(queue[2].Placement.Base)
|
|
|
|
# extend the current segment to comp for offset
|
|
segAngle = D.angle(v1, FreeCAD.Base.Vector(1, 0, 0), FreeCAD.Base.Vector(0, 0, -1))
|
|
xoffset = math.cos(segAngle) * offset
|
|
yoffset = math.sin(segAngle) * offset
|
|
|
|
newX = currLocation["X"] + xoffset
|
|
newY = currLocation["Y"] + yoffset
|
|
|
|
extendcommand = Path.Command('G1', {"X": newX, "Y": newY})
|
|
results.append(extendcommand)
|
|
|
|
currLocation.update(extendcommand.Parameters)
|
|
|
|
replace = None
|
|
return (results, replace)
|
|
|
|
def lineTwist(self, obj, queue, lastXY, twistCW=False):
|
|
'''returns gcode to do an arc move toward a line to perform
|
|
a corner action twist. Includes lifting and plungeing the knife'''
|
|
global currLocation
|
|
pivotheight = obj.pivotheight
|
|
offset = obj.offset
|
|
|
|
results = []
|
|
|
|
# set the correct twist command
|
|
if twistCW is False:
|
|
arcdir = "G3"
|
|
else:
|
|
arcdir = "G2"
|
|
|
|
# move to pivot height
|
|
zdepth = currLocation["Z"]
|
|
retract = Path.Command("G0", {"Z": pivotheight})
|
|
results.append(retract)
|
|
currLocation.update(retract.Parameters)
|
|
|
|
C = queue[1].Placement.Base
|
|
|
|
# get the vectors between endpoints to calculate twist
|
|
v2 = queue[0].Placement.Base.sub(queue[1].Placement.Base)
|
|
|
|
# calc arc endpoints to twist to
|
|
segAngle = D.angle(v2, FreeCAD.Base.Vector(1, 0, 0), FreeCAD.Base.Vector(0, 0, -1))
|
|
xoffset = math.cos(segAngle) * offset
|
|
yoffset = math.sin(segAngle) * offset
|
|
newX = queue[1].x + xoffset
|
|
newY = queue[1].y + yoffset
|
|
|
|
offsetvector = C.sub(lastXY)
|
|
I = offsetvector.x
|
|
J = offsetvector.y
|
|
|
|
# add the arc move
|
|
arcmove = Path.Command(
|
|
arcdir, {"X": newX, "Y": newY, "I": I, "J": J}) # add G2/G3 move
|
|
results.append(arcmove)
|
|
|
|
currLocation.update(arcmove.Parameters)
|
|
|
|
# plunge back to depth
|
|
plunge = Path.Command("G1", {"Z": zdepth})
|
|
results.append(plunge)
|
|
currLocation.update(plunge.Parameters)
|
|
|
|
replace = None
|
|
return (results, replace)
|
|
|
|
def execute(self, obj):
|
|
newpath = []
|
|
global currLocation
|
|
|
|
if not obj.Base:
|
|
return
|
|
|
|
if not obj.Base.isDerivedFrom("Path::Feature"):
|
|
return
|
|
|
|
if obj.Base.Path.Commands:
|
|
|
|
firstmove = Path.Command("G0", {"X": 0, "Y": 0, "Z": 0})
|
|
currLocation.update(firstmove.Parameters)
|
|
|
|
queue = []
|
|
|
|
for curCommand in obj.Base.Path.Commands:
|
|
replace = None
|
|
# don't worry about non-move commands, just add to output
|
|
if curCommand.Name not in movecommands + rapidcommands:
|
|
newpath.append(curCommand)
|
|
continue
|
|
|
|
# rapid retract triggers exit move, else just add to output
|
|
if curCommand.Name in rapidcommands:
|
|
if (curCommand.z > obj.pivotheight) and (len(queue) == 3):
|
|
# Process the exit move
|
|
tempqueue = queue
|
|
tempqueue.insert(0, curCommand)
|
|
|
|
if queue[1].Name in ['G01', 'G1']:
|
|
temp = self.lineExtension(obj, tempqueue)
|
|
newpath.extend(temp[0])
|
|
lastxy = temp[0][-1].Placement.Base
|
|
elif queue[1].Name in arccommands:
|
|
temp = self.arcExtension(obj, tempqueue)
|
|
newpath.extend(temp[0])
|
|
lastxy = temp[0][-1].Placement.Base
|
|
|
|
newpath.append(curCommand)
|
|
currLocation.update(curCommand.Parameters)
|
|
queue = []
|
|
continue
|
|
|
|
# keep a queue of feed moves and check for needed corners
|
|
if curCommand.Name in movecommands:
|
|
changedXYFlag = False
|
|
if queue:
|
|
if (curCommand.x != queue[0].x) or (curCommand.y != queue[0].y):
|
|
queue.insert(0, curCommand)
|
|
if len(queue) > 3:
|
|
queue.pop()
|
|
changedXYFlag = True
|
|
else:
|
|
queue = [curCommand]
|
|
|
|
# vertical feeding to depth
|
|
if curCommand.z != currLocation["Z"]:
|
|
newpath.append(curCommand)
|
|
currLocation.update(curCommand.Parameters)
|
|
continue
|
|
|
|
# Corner possibly needed
|
|
if changedXYFlag and (len(queue) == 3):
|
|
|
|
# check if the inciden angle incident exceeds the filter
|
|
incident_angle = self.getIncidentAngle(queue)
|
|
|
|
if abs(incident_angle) >= obj.filterangle:
|
|
if self.shortcut(queue) == "CW":
|
|
#if incident_angle >= 0:
|
|
twistCW = True
|
|
else:
|
|
twistCW = False
|
|
#
|
|
# DO THE EXTENSION
|
|
#
|
|
if queue[1].Name in ['G01', 'G1']:
|
|
temp = self.lineExtension(obj, queue)
|
|
newpath.extend(temp[0])
|
|
replace = temp[1]
|
|
lastxy = temp[0][-1].Placement.Base
|
|
elif queue[1].Name in arccommands:
|
|
temp = self.arcExtension(obj, queue)
|
|
newpath.extend(temp[0])
|
|
replace = temp[1]
|
|
lastxy = temp[0][-1].Placement.Base
|
|
else:
|
|
FreeCAD.Console.PrintWarning("I don't know what's up")
|
|
#
|
|
# DO THE TWIST
|
|
#
|
|
if queue[0].Name in ['G01', 'G1']:
|
|
temp = self.lineTwist(obj, queue, lastxy, twistCW)
|
|
replace = temp[1]
|
|
newpath.extend(temp[0])
|
|
elif queue[0].Name in arccommands:
|
|
temp = self.arcTwist(obj, queue, lastxy, twistCW)
|
|
replace = temp[1]
|
|
newpath.extend(temp[0])
|
|
else:
|
|
FreeCAD.Console.PrintWarning("I don't know what's up")
|
|
if replace is None:
|
|
newpath.append(curCommand)
|
|
else:
|
|
newpath.append(replace)
|
|
currLocation.update(curCommand.Parameters)
|
|
continue
|
|
|
|
commands = newpath
|
|
path = Path.Path(commands)
|
|
obj.Path = path
|
|
|
|
|
|
class ViewProviderDressup:
|
|
|
|
def __init__(self, vobj):
|
|
vobj.Proxy = self
|
|
|
|
def attach(self, vobj):
|
|
self.Object = vobj.Object
|
|
return
|
|
|
|
|
|
def unsetEdit(self, vobj, mode=0):
|
|
return False
|
|
|
|
def setEdit(self, vobj, mode=0):
|
|
return True
|
|
|
|
def claimChildren(self):
|
|
for i in self.Object.Base.InList:
|
|
if hasattr(i, "Group"):
|
|
group = i.Group
|
|
for g in group:
|
|
if g.Name == self.Object.Base.Name:
|
|
group.remove(g)
|
|
i.Group = group
|
|
print i.Group
|
|
#FreeCADGui.ActiveDocument.getObject(obj.Base.Name).Visibility = False
|
|
return [self.Object.Base]
|
|
|
|
def __getstate__(self):
|
|
return None
|
|
|
|
def __setstate__(self, state):
|
|
return None
|
|
|
|
def onDelete(self, arg1=None, arg2=None):
|
|
FreeCADGui.ActiveDocument.getObject(arg1.Object.Base.Name).Visibility = True
|
|
P.addToProject(arg1.Object.Base)
|
|
return True
|
|
|
|
|
|
class CommandDressupDragknife:
|
|
|
|
def GetResources(self):
|
|
return {'Pixmap': 'Path-Dressup',
|
|
'MenuText': QtCore.QT_TRANSLATE_NOOP("PathDressup_DragKnife", "DragKnife Dress-up"),
|
|
'ToolTip': QtCore.QT_TRANSLATE_NOOP("PathDressup_DragKnife", "Modifies a path to add dragknife corner actions")}
|
|
|
|
def IsActive(self):
|
|
if FreeCAD.ActiveDocument is not None:
|
|
for o in FreeCAD.ActiveDocument.Objects:
|
|
if o.Name[:3] == "Job":
|
|
return True
|
|
return False
|
|
|
|
def Activated(self):
|
|
|
|
# check that the selection contains exactly what we want
|
|
selection = FreeCADGui.Selection.getSelection()
|
|
if len(selection) != 1:
|
|
FreeCAD.Console.PrintError(
|
|
translate("PathDressup_DragKnife", "Please select one path object\n"))
|
|
return
|
|
if not selection[0].isDerivedFrom("Path::Feature"):
|
|
FreeCAD.Console.PrintError(
|
|
translate("PathDressup_DragKnife", "The selected object is not a path\n"))
|
|
return
|
|
if selection[0].isDerivedFrom("Path::FeatureCompoundPython"):
|
|
FreeCAD.Console.PrintError(
|
|
translate("PathDressup_DragKnife", "Please select a Path object"))
|
|
return
|
|
|
|
# everything ok!
|
|
FreeCAD.ActiveDocument.openTransaction(translate("PathDressup_DragKnife", "Create Dress-up"))
|
|
FreeCADGui.addModule("PathScripts.PathDressupDragknife")
|
|
FreeCADGui.addModule("PathScripts.PathUtils")
|
|
FreeCADGui.doCommand('obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython","DragknifeDressup")')
|
|
FreeCADGui.doCommand('PathScripts.PathDressupDragknife.ObjectDressup(obj)')
|
|
FreeCADGui.doCommand('obj.Base = FreeCAD.ActiveDocument.' + selection[0].Name)
|
|
FreeCADGui.doCommand('PathScripts.PathDressupDragknife.ViewProviderDressup(obj.ViewObject)')
|
|
FreeCADGui.doCommand('PathScripts.PathUtils.addToJob(obj)')
|
|
FreeCADGui.doCommand('Gui.ActiveDocument.getObject(obj.Base.Name).Visibility = False')
|
|
FreeCADGui.doCommand('obj.filterangle = 20')
|
|
FreeCADGui.doCommand('obj.offset = 2')
|
|
FreeCADGui.doCommand('obj.pivotheight = 4')
|
|
|
|
FreeCAD.ActiveDocument.commitTransaction()
|
|
FreeCAD.ActiveDocument.recompute()
|
|
|
|
|
|
if FreeCAD.GuiUp:
|
|
# register the FreeCAD command
|
|
FreeCADGui.addCommand('PathDressup_DragKnife', CommandDressupDragknife())
|
|
|
|
FreeCAD.Console.PrintLog("Loading PathDressup_DragKnife... done\n")
|