511 lines
19 KiB
Python
511 lines
19 KiB
Python
# -*- coding: utf-8 -*-
|
|
# ***************************************************************************
|
|
# * Copyright (c) 2017 LTS <SammelLothar@gmx.de> under LGPL *
|
|
# * Copyright (c) 2020-2021 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) *
|
|
# * 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 as App
|
|
import FreeCADGui
|
|
import Path
|
|
import Path.Base.Language as PathLanguage
|
|
import Path.Dressup.Utils as PathDressup
|
|
import PathScripts.PathUtils as PathUtils
|
|
from Path.Geom import wireForPath
|
|
import math
|
|
|
|
__doc__ = """LeadInOut Dressup USE ROLL-ON ROLL-OFF to profile"""
|
|
|
|
from PySide.QtCore import QT_TRANSLATE_NOOP
|
|
|
|
from PathPythonGui.simple_edit_panel import SimpleEditPanel
|
|
|
|
translate = App.Qt.translate
|
|
|
|
if False:
|
|
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
|
|
Path.Log.trackModule(Path.Log.thisModule())
|
|
else:
|
|
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
|
|
|
|
|
|
class ObjectDressup:
|
|
def __init__(self, obj):
|
|
lead_styles = [
|
|
QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "Arc"),
|
|
QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "Tangent"),
|
|
QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "Perpendicular"),
|
|
]
|
|
self.obj = obj
|
|
obj.addProperty(
|
|
"App::PropertyLink",
|
|
"Base",
|
|
"Path",
|
|
QT_TRANSLATE_NOOP("App::Property", "The base toolpath to modify"),
|
|
)
|
|
obj.addProperty(
|
|
"App::PropertyBool",
|
|
"LeadIn",
|
|
"Path",
|
|
QT_TRANSLATE_NOOP("App::Property", "Calculate roll-on to toolpath"),
|
|
)
|
|
obj.addProperty(
|
|
"App::PropertyBool",
|
|
"LeadOut",
|
|
"Path",
|
|
QT_TRANSLATE_NOOP("App::Property", "Calculate roll-off from toolpath"),
|
|
)
|
|
obj.addProperty(
|
|
"App::PropertyBool",
|
|
"KeepToolDown",
|
|
"Path",
|
|
QT_TRANSLATE_NOOP("App::Property", "Keep the Tool Down in toolpath"),
|
|
)
|
|
obj.addProperty(
|
|
"App::PropertyDistance",
|
|
"Length",
|
|
"Path",
|
|
QT_TRANSLATE_NOOP("App::Property", "Length or Radius of the approach"),
|
|
)
|
|
obj.addProperty(
|
|
"App::PropertyDistance",
|
|
"LengthOut",
|
|
"Path",
|
|
QT_TRANSLATE_NOOP("App::Property", "Length or Radius of the exit"),
|
|
)
|
|
obj.addProperty(
|
|
"App::PropertyEnumeration",
|
|
"StyleOn",
|
|
"Path",
|
|
QT_TRANSLATE_NOOP("App::Property", "The Style of motion into the toolpath"),
|
|
)
|
|
obj.StyleOn = lead_styles
|
|
obj.addProperty(
|
|
"App::PropertyEnumeration",
|
|
"StyleOff",
|
|
"Path",
|
|
QT_TRANSLATE_NOOP("App::Property", "The Style of motion out of the toolpath"),
|
|
)
|
|
obj.StyleOff = lead_styles
|
|
obj.addProperty(
|
|
"App::PropertyDistance",
|
|
"ExtendLeadIn",
|
|
"Path",
|
|
QT_TRANSLATE_NOOP("App::Property", "Extends LeadIn distance"),
|
|
)
|
|
obj.addProperty(
|
|
"App::PropertyDistance",
|
|
"ExtendLeadOut",
|
|
"Path",
|
|
QT_TRANSLATE_NOOP("App::Property", "Extends LeadOut distance"),
|
|
)
|
|
obj.addProperty(
|
|
"App::PropertyBool",
|
|
"RapidPlunge",
|
|
"Path",
|
|
QT_TRANSLATE_NOOP("App::Property", "Perform plunges with G0"),
|
|
)
|
|
obj.addProperty(
|
|
"App::PropertyBool",
|
|
"IncludeLayers",
|
|
"Path",
|
|
QT_TRANSLATE_NOOP("App::Property", "Apply LeadInOut to layers within an operation"),
|
|
)
|
|
obj.Proxy = self
|
|
|
|
self.wire = None
|
|
self.rapids = None
|
|
|
|
def dumps(self):
|
|
return None
|
|
|
|
def loads(self, state):
|
|
return None
|
|
|
|
def setup(self, obj):
|
|
obj.Length = PathDressup.toolController(obj.Base).Tool.Diameter * 0.75
|
|
obj.LengthOut = PathDressup.toolController(obj.Base).Tool.Diameter * 0.75
|
|
obj.LeadIn = True
|
|
obj.LeadOut = True
|
|
obj.KeepToolDown = False
|
|
obj.StyleOn = "Arc"
|
|
obj.StyleOff = "Arc"
|
|
obj.ExtendLeadIn = 0
|
|
obj.ExtendLeadOut = 0
|
|
obj.RapidPlunge = False
|
|
obj.IncludeLayers = True
|
|
|
|
def execute(self, obj):
|
|
if not obj.Base:
|
|
return
|
|
if not obj.Base.isDerivedFrom("Path::Feature"):
|
|
return
|
|
if not obj.Base.Path:
|
|
return
|
|
|
|
if obj.Length <= 0:
|
|
Path.Log.error(
|
|
translate("CAM_DressupLeadInOut", "Length/Radius positive not Null") + "\n"
|
|
)
|
|
obj.Length = 0.1
|
|
|
|
if obj.LengthOut <= 0:
|
|
Path.Log.error(
|
|
translate("CAM_DressupLeadInOut", "Length/Radius positive not Null") + "\n"
|
|
)
|
|
obj.LengthOut = 0.1
|
|
|
|
self.wire, self.rapids = wireForPath(PathUtils.getPathWithPlacement(obj.Base))
|
|
obj.Path = self.generateLeadInOutCurve(obj)
|
|
|
|
def getDirectionOfPath(self, obj):
|
|
op = PathDressup.baseOp(obj.Base)
|
|
side = op.Side if hasattr(op, "Side") else "Inside"
|
|
direction = op.Direction if hasattr(op, "Direction") else "CCW"
|
|
|
|
if side == "Outside":
|
|
return "left" if direction == "CW" else "right"
|
|
else:
|
|
return "right" if direction == "CW" else "left"
|
|
|
|
def getArcDirection(self, obj):
|
|
direction = self.getDirectionOfPath(obj)
|
|
return math.pi / 2 if direction == "left" else -math.pi / 2
|
|
|
|
def getTravelStart(self, obj, pos, first):
|
|
op = PathDressup.baseOp(obj.Base)
|
|
vertfeed = PathDressup.toolController(obj.Base).VertFeed.Value
|
|
travel = []
|
|
|
|
# begin positions for travel and plunge moves are not used anywhere,
|
|
# skipping them makes our life a lot easier
|
|
|
|
# move to clearance height
|
|
if first:
|
|
travel.append(PathLanguage.MoveStraight(None, "G0", {"Z": op.ClearanceHeight.Value}))
|
|
|
|
# move to correct xy-position
|
|
travel.append(PathLanguage.MoveStraight(None, "G0", {"X": pos.x, "Y": pos.y}))
|
|
|
|
# move to correct z-position (either rapidly or in two steps)
|
|
if obj.RapidPlunge:
|
|
travel.append(PathLanguage.MoveStraight(None, "G0", {"Z": pos.z}))
|
|
else:
|
|
if first or not obj.KeepToolDown:
|
|
travel.append(PathLanguage.MoveStraight(None, "G0", {"Z": op.SafeHeight.Value}))
|
|
travel.append(PathLanguage.MoveStraight(None, "G1", {"Z": pos.z, "F": vertfeed}))
|
|
|
|
return travel
|
|
|
|
def getTravelEnd(self, obj, pos, last):
|
|
op = PathDressup.baseOp(obj.Base)
|
|
travel = []
|
|
|
|
# move to clearance height
|
|
if last or not obj.KeepToolDown:
|
|
travel.append(PathLanguage.MoveStraight(None, "G0", {"Z": op.ClearanceHeight.Value}))
|
|
|
|
return travel
|
|
|
|
def angleToVector(self, angle):
|
|
return App.Vector(math.cos(angle), math.sin(angle), 0)
|
|
|
|
def createArcMove(self, obj, begin, end, c):
|
|
horizfeed = PathDressup.toolController(obj.Base).HorizFeed.Value
|
|
|
|
param = {"X": end.x, "Y": end.y, "I": c.x, "J": c.y, "F": horizfeed}
|
|
if self.getArcDirection(obj) > 0:
|
|
return PathLanguage.MoveArcCCW(begin, "G3", param)
|
|
else:
|
|
return PathLanguage.MoveArcCW(begin, "G2", param)
|
|
|
|
def createStraightMove(self, obj, begin, end):
|
|
horizfeed = PathDressup.toolController(obj.Base).HorizFeed.Value
|
|
|
|
param = {"X": end.x, "Y": end.y, "F": horizfeed}
|
|
return PathLanguage.MoveStraight(begin, "G1", param)
|
|
|
|
def getLeadStart(self, obj, move, first):
|
|
lead = []
|
|
begin = move.positionBegin()
|
|
|
|
def prepend(instr):
|
|
nonlocal lead
|
|
nonlocal begin
|
|
lead.insert(0, instr)
|
|
begin = lead[0].positionBegin()
|
|
|
|
# tangent begin move
|
|
# <----_-----x-------------------x
|
|
# / |
|
|
# / | normal
|
|
# | |
|
|
# x v
|
|
|
|
if obj.LeadIn:
|
|
length = obj.Length.Value
|
|
angle = move.anglesOfTangents()[0]
|
|
tangent = -self.angleToVector(angle) * length
|
|
normal = self.angleToVector(angle + self.getArcDirection(obj)) * length
|
|
|
|
# prepend the selected lead-in
|
|
if obj.StyleOn == "Arc":
|
|
arcbegin = begin + tangent + normal
|
|
prepend(self.createArcMove(obj, arcbegin, begin, -tangent))
|
|
elif obj.StyleOn == "Tangent":
|
|
prepend(self.createStraightMove(obj, begin + tangent, begin))
|
|
else: # obj.StyleOn == "Perpendicular"
|
|
prepend(self.createStraightMove(obj, begin + normal, begin))
|
|
|
|
extend = obj.ExtendLeadIn.Value
|
|
if extend != 0:
|
|
# prepend extension
|
|
extendbegin = begin + normal / length * extend
|
|
prepend(self.createStraightMove(obj, extendbegin, begin))
|
|
|
|
# prepend travel moves
|
|
lead = self.getTravelStart(obj, begin, first) + lead
|
|
|
|
return lead
|
|
|
|
def getLeadEnd(self, obj, move, last):
|
|
lead = []
|
|
end = move.positionEnd()
|
|
|
|
def append(instr):
|
|
nonlocal lead
|
|
nonlocal end
|
|
lead.append(instr)
|
|
end = lead[-1].positionEnd()
|
|
|
|
# move end tangent
|
|
# x-------------------x-----_---->
|
|
# | \
|
|
# normal | \
|
|
# | |
|
|
# v x
|
|
|
|
if obj.LeadOut:
|
|
length = obj.LengthOut.Value
|
|
angle = move.anglesOfTangents()[1]
|
|
tangent = self.angleToVector(angle) * length
|
|
normal = self.angleToVector(angle + self.getArcDirection(obj)) * length
|
|
|
|
# append the selected lead-out
|
|
if obj.StyleOff == "Arc":
|
|
arcend = end + tangent + normal
|
|
append(self.createArcMove(obj, end, arcend, normal))
|
|
elif obj.StyleOff == "Tangent":
|
|
append(self.createStraightMove(obj, end, end + tangent))
|
|
else: # obj.StyleOff == "Perpendicular"
|
|
append(self.createStraightMove(obj, end, end + normal))
|
|
|
|
extend = obj.ExtendLeadOut.Value
|
|
if extend != 0:
|
|
# append extension
|
|
extendend = end + normal / length * extend
|
|
append(self.createStraightMove(obj, end, extendend))
|
|
|
|
# append travel moves
|
|
lead += self.getTravelEnd(obj, end, last)
|
|
|
|
return lead
|
|
|
|
def isCuttingMove(self, obj, instr):
|
|
return (
|
|
instr.isMove()
|
|
and not instr.isRapid()
|
|
and (not obj.IncludeLayers or not instr.isPlunge())
|
|
)
|
|
|
|
def findLastCuttingMoveIndex(self, obj, source):
|
|
for i in range(len(source) - 1, -1, -1):
|
|
if self.isCuttingMove(obj, source[i]):
|
|
return i
|
|
return None
|
|
|
|
def generateLeadInOutCurve(self, obj):
|
|
source = PathLanguage.Maneuver.FromPath(PathUtils.getPathWithPlacement(obj.Base)).instr
|
|
maneuver = PathLanguage.Maneuver()
|
|
|
|
# Knowing weather a given instruction is the first cutting move is easy,
|
|
# we just use a flag and set it to false afterwards. To find the last
|
|
# cutting move we need to search the list in reverse order.
|
|
first = True
|
|
lastCuttingMoveIndex = self.findLastCuttingMoveIndex(obj, source)
|
|
|
|
for i, instr in enumerate(source):
|
|
if not self.isCuttingMove(obj, instr):
|
|
# non-move instructions get added verbatim
|
|
if not instr.isMove():
|
|
maneuver.addInstruction(instr)
|
|
|
|
# skip travel and plunge moves, travel moves will be added in
|
|
# getLeadStart and getLeadEnd
|
|
continue
|
|
|
|
if first or not self.isCuttingMove(obj, source[i - 1]):
|
|
# add lead start and travel moves
|
|
maneuver.addInstructions(self.getLeadStart(obj, instr, first))
|
|
first = False
|
|
|
|
# add current move
|
|
maneuver.addInstruction(instr)
|
|
|
|
last = i == lastCuttingMoveIndex
|
|
if last or not self.isCuttingMove(obj, source[i + 1]):
|
|
# add lead end and travel moves
|
|
maneuver.addInstructions(self.getLeadEnd(obj, instr, last))
|
|
|
|
return maneuver.toPath()
|
|
|
|
|
|
class TaskDressupLeadInOut(SimpleEditPanel):
|
|
_transaction_name = "Edit LeadInOut Dress-up"
|
|
_ui_file = ":/panels/DressUpLeadInOutEdit.ui"
|
|
|
|
def setupUi(self):
|
|
self.connectWidget("LeadIn", self.form.chkLeadIn)
|
|
self.connectWidget("LeadOut", self.form.chkLeadOut)
|
|
self.connectWidget("Length", self.form.dspLenIn)
|
|
self.connectWidget("LengthOut", self.form.dspLenOut)
|
|
self.connectWidget("ExtendLeadIn", self.form.dspExtendIn)
|
|
self.connectWidget("ExtendLeadOut", self.form.dspExtendOut)
|
|
self.connectWidget("StyleOn", self.form.cboStyleIn)
|
|
self.connectWidget("StyleOff", self.form.cboStyleOut)
|
|
self.connectWidget("RapidPlunge", self.form.chkRapidPlunge)
|
|
self.connectWidget("IncludeLayers", self.form.chkLayers)
|
|
self.connectWidget("KeepToolDown", self.form.chkKeepToolDown)
|
|
self.setFields()
|
|
|
|
|
|
class ViewProviderDressup:
|
|
def __init__(self, vobj):
|
|
self.obj = vobj.Object
|
|
self.setEdit(vobj)
|
|
|
|
def attach(self, vobj):
|
|
self.obj = vobj.Object
|
|
self.panel = None
|
|
|
|
def claimChildren(self):
|
|
if hasattr(self.obj.Base, "InList"):
|
|
for i in self.obj.Base.InList:
|
|
if hasattr(i, "Group"):
|
|
group = i.Group
|
|
for g in group:
|
|
if g.Name == self.obj.Base.Name:
|
|
group.remove(g)
|
|
i.Group = group
|
|
return [self.obj.Base]
|
|
|
|
def setEdit(self, vobj, mode=0):
|
|
FreeCADGui.Control.closeDialog()
|
|
panel = TaskDressupLeadInOut(vobj.Object, self)
|
|
FreeCADGui.Control.showDialog(panel)
|
|
return True
|
|
|
|
def unsetEdit(self, vobj, mode=0):
|
|
if self.panel:
|
|
self.panel.abort()
|
|
|
|
def onDelete(self, arg1=None, arg2=None):
|
|
"""this makes sure that the base operation is added back to the project and visible"""
|
|
Path.Log.debug("Deleting Dressup")
|
|
if arg1.Object and arg1.Object.Base:
|
|
FreeCADGui.ActiveDocument.getObject(arg1.Object.Base.Name).Visibility = True
|
|
job = PathUtils.findParentJob(self.obj)
|
|
if job:
|
|
job.Proxy.addOperation(arg1.Object.Base, arg1.Object)
|
|
arg1.Object.Base = None
|
|
return True
|
|
|
|
def dumps(self):
|
|
return None
|
|
|
|
def loads(self, state):
|
|
return None
|
|
|
|
def clearTaskPanel(self):
|
|
self.panel = None
|
|
|
|
|
|
class CommandPathDressupLeadInOut:
|
|
def GetResources(self):
|
|
return {
|
|
"Pixmap": "CAM_Dressup",
|
|
"MenuText": QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "LeadInOut"),
|
|
"ToolTip": QT_TRANSLATE_NOOP(
|
|
"CAM_DressupLeadInOut",
|
|
"Creates a Cutter Radius Compensation G41/G42 Entry Dressup object from a selected path",
|
|
),
|
|
}
|
|
|
|
def IsActive(self):
|
|
op = PathDressup.selection()
|
|
if op:
|
|
return not PathDressup.hasEntryMethod(op)
|
|
return False
|
|
|
|
def Activated(self):
|
|
# check that the selection contains exactly what we want
|
|
selection = FreeCADGui.Selection.getSelection()
|
|
if len(selection) != 1:
|
|
Path.Log.error(
|
|
translate("CAM_DressupLeadInOut", "Please select one toolpath object") + "\n"
|
|
)
|
|
return
|
|
baseObject = selection[0]
|
|
if not baseObject.isDerivedFrom("Path::Feature"):
|
|
Path.Log.error(
|
|
translate("CAM_DressupLeadInOut", "The selected object is not a toolpath") + "\n"
|
|
)
|
|
return
|
|
if baseObject.isDerivedFrom("Path::FeatureCompoundPython"):
|
|
Path.Log.error(translate("CAM_DressupLeadInOut", "Please select a Profile object"))
|
|
return
|
|
|
|
# everything ok!
|
|
App.ActiveDocument.openTransaction("Create LeadInOut Dressup")
|
|
FreeCADGui.addModule("Path.Dressup.Gui.LeadInOut")
|
|
FreeCADGui.addModule("PathScripts.PathUtils")
|
|
FreeCADGui.doCommand(
|
|
'obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", "LeadInOutDressup")'
|
|
)
|
|
FreeCADGui.doCommand("dbo = Path.Dressup.Gui.LeadInOut.ObjectDressup(obj)")
|
|
FreeCADGui.doCommand("base = FreeCAD.ActiveDocument." + selection[0].Name)
|
|
FreeCADGui.doCommand("job = PathScripts.PathUtils.findParentJob(base)")
|
|
FreeCADGui.doCommand("obj.Base = base")
|
|
FreeCADGui.doCommand("job.Proxy.addOperation(obj, base)")
|
|
FreeCADGui.doCommand("dbo.setup(obj)")
|
|
FreeCADGui.doCommand(
|
|
"obj.ViewObject.Proxy = Path.Dressup.Gui.LeadInOut.ViewProviderDressup(obj.ViewObject)"
|
|
)
|
|
FreeCADGui.doCommand("Gui.ActiveDocument.getObject(base.Name).Visibility = False")
|
|
App.ActiveDocument.commitTransaction()
|
|
App.ActiveDocument.recompute()
|
|
|
|
|
|
if App.GuiUp:
|
|
# register the FreeCAD command
|
|
FreeCADGui.addCommand("CAM_DressupLeadInOut", CommandPathDressupLeadInOut())
|
|
|
|
Path.Log.notice("Loading CAM_DressupLeadInOut... done\n")
|