Files
create/src/Mod/CAM/Path/Dressup/Gui/LeadInOut.py
2025-10-23 06:48:14 +03:00

1307 lines
52 KiB
Python

# ***************************************************************************
# * 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.Base.Util import toolControllerForOp
import copy
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())
lead_styles = (
# common options first
QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "Arc"),
QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "Line"),
# additional options, alphabetical order
QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "Arc3d"),
QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "ArcZ"),
QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "Helix"),
QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "Line3d"),
QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "LineZ"),
QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "No Retract"),
QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "Perpendicular"),
QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "Tangent"),
QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "Vertical"),
)
class ObjectDressup:
def __init__(self, obj):
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", "Modify lead in to toolpath"),
)
obj.addProperty(
"App::PropertyBool",
"LeadOut",
"Path",
QT_TRANSLATE_NOOP("App::Property", "Modify lead out from toolpath"),
)
obj.addProperty(
"App::PropertyLength",
"RetractThreshold",
"Path",
QT_TRANSLATE_NOOP(
"App::Property", "Set distance which will attempts to avoid unnecessary retractions"
),
)
obj.addProperty(
"App::PropertyEnumeration",
"StyleIn",
"Path",
QT_TRANSLATE_NOOP("App::Property", "The style of motion into the toolpath"),
)
obj.StyleIn = lead_styles
obj.addProperty(
"App::PropertyEnumeration",
"StyleOut",
"Path",
QT_TRANSLATE_NOOP("App::Property", "The style of motion out of the toolpath"),
)
obj.StyleOut = lead_styles
obj.addProperty(
"App::PropertyBool",
"RapidPlunge",
"Path",
QT_TRANSLATE_NOOP("App::Property", "Perform plunges with G0"),
)
obj.addProperty(
"App::PropertyAngle",
"AngleIn",
"Path Lead-in",
QT_TRANSLATE_NOOP("App::Property", "Angle of the Lead-In (1..90)"),
)
obj.addProperty(
"App::PropertyAngle",
"AngleOut",
"Path Lead-out",
QT_TRANSLATE_NOOP("App::Property", "Angle of the Lead-Out (1..90)"),
)
obj.addProperty(
"App::PropertyLength",
"RadiusIn",
"Path Lead-in",
QT_TRANSLATE_NOOP("App::Property", "Determine length of the Lead-In"),
)
obj.addProperty(
"App::PropertyLength",
"RadiusOut",
"Path Lead-out",
QT_TRANSLATE_NOOP("App::Property", "Determine length of the Lead-Out"),
)
obj.addProperty(
"App::PropertyBool",
"InvertIn",
"Path Lead-in",
QT_TRANSLATE_NOOP("App::Property", "Invert Lead-In direction"),
)
obj.addProperty(
"App::PropertyBool",
"InvertOut",
"Path Lead-out",
QT_TRANSLATE_NOOP("App::Property", "Invert Lead-Out direction"),
)
obj.addProperty(
"App::PropertyDistance",
"OffsetIn",
"Path Lead-in",
QT_TRANSLATE_NOOP("App::Property", "Move start point"),
)
obj.addProperty(
"App::PropertyDistance",
"OffsetOut",
"Path Lead-out",
QT_TRANSLATE_NOOP("App::Property", "Move end point"),
)
obj.Proxy = self
def dumps(self):
return None
def loads(self, state):
return None
def setup(self, obj):
obj.LeadIn = True
obj.LeadOut = True
obj.AngleIn = 45
obj.AngleOut = 45
obj.InvertIn = False
obj.InvertOut = False
obj.RapidPlunge = False
obj.StyleIn = "Arc"
obj.StyleOut = "Arc"
baseWithTC = self.getBaseWithTC(obj)
if baseWithTC and baseWithTC.ToolController:
expr = f"{baseWithTC.Name}.ToolController.Tool.Diameter.Value/2*1.5"
obj.setExpression("RadiusIn", expr)
obj.setExpression("RadiusOut", expr)
else:
obj.RadiusIn = 10
obj.RadiusOut = 10
def getBaseWithTC(self, obj):
if hasattr(obj, "ToolController"):
return obj
if not hasattr(obj, "Base"):
return None
if isinstance(obj.Base, list) and obj.Base and obj.Base[0].isDerivedFrom("Path::Feature"):
return self.getBaseWithTC(obj.Base[0])
elif not isinstance(obj.Base, list) and obj.Base.isDerivedFrom("Path::Feature"):
return self.getBaseWithTC(obj.Base)
return None
def execute(self, obj):
if not obj.Base:
obj.Path = Path.Path()
return
if not obj.Base.isDerivedFrom("Path::Feature"):
obj.Path = Path.Path()
return
if not obj.Base.Path:
obj.Path = Path.Path()
return
if obj.RadiusIn <= 0:
obj.RadiusIn = 1
if obj.RadiusOut <= 0:
obj.RadiusOut = 1
nonZeroAngleStyles = ("Arc", "Arc3d", "ArcZ", "Helix", "LineZ")
limit_angle_in = 1 if obj.StyleIn in nonZeroAngleStyles else 0
limit_angle_out = 1 if obj.StyleOut in nonZeroAngleStyles else 0
if obj.AngleIn > 180:
obj.AngleIn = 180
if obj.AngleIn < limit_angle_in:
obj.AngleIn = limit_angle_in
if obj.AngleOut > 180:
obj.AngleOut = 180
if obj.AngleOut < limit_angle_out:
obj.AngleOut = limit_angle_out
hideModes = {
"Angle": ("No Retract", "Perpendicular", "Tangent", "Vertical"),
"Invert": ("No Retract", "ArcZ", "LineZ", "Vertical"),
"Offset": ("No Retract"),
"Radius": ("No Retract", "Vertical"),
}
for k, v in hideModes.items():
obj.setEditorMode(k + "In", 2 if obj.StyleIn in v else 0)
obj.setEditorMode(k + "Out", 2 if obj.StyleOut in v else 0)
self.baseOp = PathDressup.baseOp(obj.Base)
self.toolController = toolControllerForOp(obj.Base)
if not self.toolController:
obj.Path = Path.Path()
Path.Log.warning(
translate(
"CAM_DressupLeadInOut", "Tool controller not selected for base operation: %s"
)
% obj.Base.Label
)
return
self.horizFeed = self.toolController.HorizFeed.Value
self.vertFeed = self.toolController.VertFeed.Value
obj.Path = self.generateLeadInOutCurve(obj)
def onDocumentRestored(self, obj):
"""onDocumentRestored(obj) ... Called automatically when document is restored."""
styleOn = styleOff = None
if hasattr(obj, "StyleOn"):
# Replace StyleOn by StyleIn
styleOn = obj.StyleOn
obj.addProperty(
"App::PropertyEnumeration",
"StyleIn",
"Path",
QT_TRANSLATE_NOOP("App::Property", "The style of motion into the toolpath"),
)
obj.StyleIn = lead_styles
obj.removeProperty("StyleOn")
obj.StyleIn = "Arc"
if hasattr(obj, "StyleOff"):
# Replace StyleOff by StyleOut
styleOff = obj.StyleOff
obj.addProperty(
"App::PropertyEnumeration",
"StyleOut",
"Path",
QT_TRANSLATE_NOOP("App::Property", "The style of motion out of the toolpath"),
)
obj.StyleOut = lead_styles
obj.removeProperty("StyleOff")
obj.StyleOut = "Arc"
if not hasattr(obj, "AngleIn"):
obj.addProperty(
"App::PropertyAngle",
"AngleIn",
"Path Lead-in",
QT_TRANSLATE_NOOP("App::Property", "Angle of the Lead-In (1..90)"),
)
obj.AngleIn = 45
if not hasattr(obj, "AngleOut"):
obj.addProperty(
"App::PropertyAngle",
"AngleOut",
"Path Lead-out",
QT_TRANSLATE_NOOP("App::Property", "Angle of the Lead-Out (1..90)"),
)
obj.AngleOut = 45
if styleOn:
if styleOn == "Arc":
obj.StyleIn = "Arc"
obj.AngleIn = 90
if styleOff:
if styleOff == "Arc":
obj.StyleOut = "Arc"
obj.AngleOut = 90
for prop in ("Length", "LengthIn"):
if hasattr(obj, prop):
obj.renameProperty(prop, "RadiusIn")
break
if hasattr(obj, "LengthOut"):
obj.renameProperty("LengthOut", "RadiusOut")
if hasattr(obj, "PercentageRadiusIn") or hasattr(obj, "PercentageRadiusOut"):
baseWithTC = self.getBaseWithTC(obj)
if hasattr(obj, "PercentageRadiusIn"):
obj.addProperty(
"App::PropertyLength",
"RadiusIn",
"Path Lead-in",
QT_TRANSLATE_NOOP("App::Property", "Determine length of the Lead-In"),
)
if baseWithTC and baseWithTC.ToolController:
valIn = obj.PercentageRadiusIn / 100
exprIn = f"{baseWithTC.Name}.ToolController.Tool.Diameter.Value/2*{valIn}"
obj.setExpression("RadiusIn", exprIn)
else:
obj.RadiusIn = 10
obj.removeProperty("PercentageRadiusIn")
if hasattr(obj, "PercentageRadiusOut"):
obj.addProperty(
"App::PropertyLength",
"RadiusOut",
"Path Lead-out",
QT_TRANSLATE_NOOP("App::Property", "Determine length of the Lead-Out"),
)
if baseWithTC and baseWithTC.ToolController:
valOut = obj.PercentageRadiusOut / 100
exprOut = f"{baseWithTC.Name}.ToolController.Tool.Diameter.Value/2*{valOut}"
obj.setExpression("RadiusOut", exprOut)
else:
obj.RadiusOut = 10
obj.removeProperty("PercentageRadiusOut")
# The new features do not have a good analog for ExtendLeadIn/Out, so these old values will be ignored
if hasattr(obj, "ExtendLeadIn"):
# Remove ExtendLeadIn property
obj.removeProperty("ExtendLeadIn")
if hasattr(obj, "ExtendLeadOut"):
# Remove ExtendLeadOut property
obj.removeProperty("ExtendLeadOut")
if hasattr(obj, "IncludeLayers"):
obj.removeProperty("IncludeLayers")
if not hasattr(obj, "InvertIn"):
obj.addProperty(
"App::PropertyBool",
"InvertIn",
"Path Lead-in",
QT_TRANSLATE_NOOP("App::Property", "Invert Lead-In direction"),
)
if not hasattr(obj, "InvertOut"):
obj.addProperty(
"App::PropertyBool",
"InvertOut",
"Path Lead-out",
QT_TRANSLATE_NOOP("App::Property", "Invert Lead-Out direction"),
)
if not hasattr(obj, "OffsetIn"):
obj.addProperty(
"App::PropertyDistance",
"OffsetIn",
"Path Lead-in",
QT_TRANSLATE_NOOP("App::Property", "Move start point"),
)
if not hasattr(obj, "OffsetOut"):
obj.addProperty(
"App::PropertyDistance",
"OffsetOut",
"Path Lead-out",
QT_TRANSLATE_NOOP("App::Property", "Move end point"),
)
if not hasattr(obj, "RetractThreshold"):
obj.addProperty(
"App::PropertyLength",
"RetractThreshold",
"Path",
QT_TRANSLATE_NOOP(
"App::Property",
"Set distance which will attempts to avoid unnecessary retractions",
),
)
if hasattr(obj, "KeepToolDown"):
if obj.KeepToolDown:
obj.RetractThreshold = 999999
obj.removeProperty("KeepToolDown")
# Get direction for lead-in/lead-out in XY plane
def getLeadDir(self, obj, invert=False):
output = math.pi / 2
side = self.baseOp.Side if hasattr(self.baseOp, "Side") else "Inside"
direction = self.baseOp.Direction if hasattr(self.baseOp, "Direction") else "CCW"
if (side == "Inside" and direction == "CW") or (side == "Outside" and direction == "CCW"):
output = -output
if invert:
output = -output
return output
# Get direction of original path
def getPathDir(self, obj):
# only CW or CCW is matter
direction = self.baseOp.Direction if hasattr(self.baseOp, "Direction") else "CCW"
output = math.pi / 2
if direction == "CW":
output = -output
return output
# Create safety movements to start point
def getTravelStart(self, obj, pos, first, outInstrPrev):
commands = []
posPrev = outInstrPrev.positionEnd() if outInstrPrev else App.Vector()
posPrevXY = App.Vector(posPrev.x, posPrev.y, 0)
posXY = App.Vector(pos.x, pos.y, 0)
distance = posPrevXY.distanceToPoint(posXY)
if first or (distance > obj.RetractThreshold):
# move to clearance height
commands.append(
PathLanguage.MoveStraight(None, "G00", {"Z": self.baseOp.ClearanceHeight.Value})
)
# move to mill position at clearance height
commands.append(PathLanguage.MoveStraight(None, "G00", {"X": pos.x, "Y": pos.y}))
# move vertical down to mill position
if obj.RapidPlunge:
# move to mill position rapidly
commands.append(PathLanguage.MoveStraight(None, "G00", {"Z": pos.z}))
else:
# move to mill position in two steps
commands.append(
PathLanguage.MoveStraight(None, "G00", {"Z": self.baseOp.SafeHeight.Value})
)
commands.append(
PathLanguage.MoveStraight(None, "G01", {"Z": pos.z, "F": self.vertFeed})
)
else:
# move to next mill position by short path
if obj.RapidPlunge:
commands.append(
PathLanguage.MoveStraight(None, "G00", {"X": pos.x, "Y": pos.y, "Z": pos.z})
)
else:
commands.append(
PathLanguage.MoveStraight(
None, "G01", {"X": pos.x, "Y": pos.y, "Z": pos.z, "F": self.vertFeed}
)
)
return commands
# Create commands with movements to clearance height
def getTravelEnd(self, obj):
commands = []
z = self.baseOp.ClearanceHeight.Value
commands.append(PathLanguage.MoveStraight(None, "G00", {"Z": z}))
return commands
# Create vector object from angle
def angleToVector(self, angle):
return App.Vector(math.cos(angle), math.sin(angle), 0)
# Create arc in XY plane with automatic detection G2|G3
def createArcMove(self, obj, begin, end, offset, invert=False):
param = {
"X": end.x,
"Y": end.y,
"Z": end.z,
"I": offset.x,
"J": offset.y,
"F": self.horizFeed,
}
if self.getLeadDir(obj, invert) > 0:
command = PathLanguage.MoveArcCCW(begin, "G3", param)
else:
command = PathLanguage.MoveArcCW(begin, "G2", param)
return command
# Create arc in XY plane with manually set G2|G3
def createArcMoveN(self, obj, begin, end, offset, cmdName):
param = {"X": end.x, "Y": end.y, "I": offset.x, "J": offset.y, "F": self.horizFeed}
if cmdName == "G2":
command = PathLanguage.MoveArcCW(begin, cmdName, param)
else:
command = PathLanguage.MoveArcCCW(begin, cmdName, param)
return command
# Create line movement G1
def createStraightMove(self, obj, begin, end):
param = {"X": end.x, "Y": end.y, "Z": end.z, "F": self.horizFeed}
command = PathLanguage.MoveStraight(begin, "G1", param)
return command
# Get optimal step angle for iteration ArcZ
def getStepAngleArcZ(self, obj, radius):
job = PathUtils.findParentJob(obj)
minArcLength = job.GeometryTolerance.Value * 2
maxArcLength = 1
stepAngle = math.pi / 60
stepArcLength = stepAngle * radius
if stepArcLength > maxArcLength:
# limit max arc length by 1 mm
stepAngle = maxArcLength / radius
elif stepArcLength < minArcLength:
# limit min arc length by geometry tolerance
stepAngle = minArcLength / radius
return stepAngle
# Create vertical arc with move Down by line segments
def createArcZMoveDown(self, obj, begin, end, radius):
commands = []
angle = math.acos((radius - begin.z + end.z) / radius) # start angle
stepAngle = self.getStepAngleArcZ(obj, radius)
iters = math.ceil(angle / stepAngle)
iterBegin = copy.copy(begin) # start point of short segment
iter = 1
v = end - begin
n = math.hypot(v.x, v.y)
u = v / n
while iter <= iters:
if iter < iters:
angle -= stepAngle
distance = n - radius * math.sin(angle)
iterEnd = begin + u * distance
iterEnd.z = end.z + radius * (1 - math.cos(angle))
else:
# exclude error of calculations for the last iteration
iterEnd = copy.copy(end)
param = {"X": iterEnd.x, "Y": iterEnd.y, "Z": iterEnd.z, "F": self.horizFeed}
commands.append(PathLanguage.MoveStraight(iterBegin, "G1", param))
iterBegin = copy.copy(iterEnd)
iter += 1
return commands
# Create vertical arc with move Up by line segments
def createArcZMoveUp(self, obj, begin, end, radius):
commands = []
angleMax = math.acos((radius - end.z + begin.z) / radius) # finish angle
stepAngle = self.getStepAngleArcZ(obj, radius)
iters = math.ceil(angleMax / stepAngle)
iterBegin = copy.copy(begin) # start point of short segment
iter = 1
v = end - begin
n = math.hypot(v.x, v.y)
u = v / n
angle = 0 # start angle
while iter <= iters:
if iter < iters:
angle += stepAngle
distance = radius * math.sin(angle)
iterEnd = begin + u * distance
iterEnd.z = begin.z + radius * (1 - math.cos(angle))
else:
# exclude the error of calculations of the last point
iterEnd = copy.copy(end)
param = {"X": iterEnd.x, "Y": iterEnd.y, "Z": iterEnd.z, "F": self.horizFeed}
commands.append(PathLanguage.MoveStraight(iterBegin, "G1", param))
iterBegin = copy.copy(iterEnd)
iter += 1
return commands
def getLeadStart(self, obj, move, first, inInstrPrev, outInstrPrev):
# tangent begin move
# <----_-----x-------------------x
# / |
# / | normal
# | |
# x v
lead = []
begin = move.positionBegin()
beginZ = move.positionBegin().z # do not change this variable below
if not obj.LeadIn and obj.LeadOut:
# can not skip leadin if leadout
# override style to get correct move to next step down
styleIn = "Vertical"
else:
styleIn = obj.StyleIn
if styleIn not in ("No Retract", "Vertical"):
if styleIn == "Perpendicular":
angleIn = math.pi / 2
elif styleIn == "Tangent":
angleIn = 0
else:
angleIn = math.radians(obj.AngleIn.Value)
length = obj.RadiusIn.Value
angleTangent = move.anglesOfTangents()[0]
normalMax = (
self.angleToVector(angleTangent + self.getLeadDir(obj, obj.InvertIn)) * length
)
# Here you can find description of the calculations
# https://forum.freecad.org/viewtopic.php?t=97641
# prepend "Arc" style lead-in - arc in XY
# Arc3d the same as Arc, but increased Z start point
if styleIn in ("Arc", "Arc3d", "Helix"):
# tangent and normal vectors in XY plane
arcRadius = length
tangentLength = math.sin(angleIn) * arcRadius
normalLength = arcRadius * (1 - math.cos(angleIn))
tangent = -self.angleToVector(angleTangent) * tangentLength
normal = (
self.angleToVector(angleTangent + self.getLeadDir(obj, obj.InvertIn))
* normalLength
)
arcBegin = begin + tangent + normal
arcCenter = begin + normalMax
arcOffset = arcCenter - arcBegin
lead.append(self.createArcMove(obj, arcBegin, begin, arcOffset, obj.InvertIn))
# prepend "Line" style lead-in - line in XY
# Line3d the same as Line, but increased Z start point
elif styleIn in ("Line", "Line3d", "Perpendicular", "Tangent"):
# tangent and normal vectors in XY plane
tangentLength = math.cos(angleIn) * length
normalLength = math.sin(angleIn) * length
tangent = -self.angleToVector(angleTangent) * tangentLength
normal = (
self.angleToVector(angleTangent + self.getLeadDir(obj, obj.InvertIn))
* normalLength
)
lineBegin = begin + tangent + normal
lead.append(self.createStraightMove(obj, lineBegin, begin))
# prepend "LineZ" style lead-in - vertical inclined line
# Should be apply only on straight Path segment
elif styleIn == "LineZ":
# tangent vector in XY plane
# normal vector is vertical
normalLengthMax = self.baseOp.SafeHeight.Value - begin.z
normalLength = math.sin(angleIn) * length
# do not exceed Normal vector max length
normalLength = min(normalLength, normalLengthMax)
tangentLength = normalLength / math.tan(angleIn)
tangent = -self.angleToVector(angleTangent) * tangentLength
normal = App.Vector(0, 0, normalLength)
lineBegin = begin + tangent + normal
lead.append(self.createStraightMove(obj, lineBegin, begin))
# prepend "ArcZ" style lead-in - vertical Arc
# Should be apply only on straight Path segment
elif styleIn == "ArcZ":
# tangent vector in XY plane
# normal vector is vertical
arcRadius = length
normalLengthMax = self.baseOp.SafeHeight.Value - begin.z
normalLength = arcRadius * (1 - math.cos(angleIn))
if normalLength > normalLengthMax:
# do not exceed Normal vector max length
normalLength = normalLengthMax
# recalculate angle for limited normal length
angleIn = math.acos((arcRadius - normalLength) / arcRadius)
tangentLength = arcRadius * math.sin(angleIn)
tangent = -self.angleToVector(angleTangent) * tangentLength
normal = App.Vector(0, 0, normalLength)
arcBegin = begin + tangent + normal
lead.extend(self.createArcZMoveDown(obj, arcBegin, begin, arcRadius))
# replace 'begin' position by first lead-in command
begin = lead[0].positionBegin()
if styleIn in ("Arc3d", "Line3d"):
# up Z start point for Arc3d and Line3d
if inInstrPrev and inInstrPrev.z() > begin.z:
begin.z = inInstrPrev.z()
else:
begin.z = self.baseOp.StartDepth.Value
lead[0].setPositionBegin(begin)
elif styleIn == "Helix":
# change Z for current helix lead-in
posPrevZ = None
if outInstrPrev:
posPrevZ = outInstrPrev.positionEnd().z
if posPrevZ is not None and posPrevZ > beginZ:
halfStepZ = (posPrevZ - beginZ) / 2
begin.z += halfStepZ
else:
begin.z = self.baseOp.StartDepth.Value
if obj.StyleOut == "Helix" and outInstrPrev:
"""change Z for previous helix lead-out
Can not do it in getLeadEnd(),
because no any information about next moves there while creating Lead-out"""
posPrevZ = outInstrPrev.positionEnd().z
if posPrevZ > beginZ:
"""previous profile upper than this
mean procesing one stepdown profile"""
halfStepZ = (posPrevZ - beginZ) / 2
outInstrPrev.param["Z"] = posPrevZ - halfStepZ
# get complete start travel moves
if styleIn != "No Retract":
travelToStart = self.getTravelStart(obj, begin, first, outInstrPrev)
else:
# exclude any lead-in commands
param = {"X": begin.x, "Y": begin.y, "Z": begin.z, "F": self.horizFeed}
travelToStart = [PathLanguage.MoveStraight(None, "G01", param)]
lead = travelToStart + lead
return lead
def getLeadEnd(self, obj, move, last, outInstrPrev):
# move end tangent
# x-------------------x-----_---->
# | \
# normal | \
# | |
# v x
lead = []
end = move.positionEnd()
if obj.StyleOut not in ("No Retract", "Vertical"):
if obj.StyleOut == "Perpendicular":
angleOut = math.pi / 2
elif obj.StyleOut == "Tangent":
angleOut = 0
else:
angleOut = math.radians(obj.AngleOut.Value)
length = obj.RadiusOut.Value
angleTangent = move.anglesOfTangents()[1]
normalMax = (
self.angleToVector(angleTangent + self.getLeadDir(obj, obj.InvertOut)) * length
)
# Here you can find description of the calculations
# https://forum.freecad.org/viewtopic.php?t=97641
# append "Arc" style lead-out - arc in XY
# Arc3d the same as Arc, but increased Z start point
if obj.StyleOut in ("Arc", "Arc3d", "Helix"):
# tangent and normal vectors in XY plane
arcRadius = length
tangentLength = math.sin(angleOut) * arcRadius
normalLength = arcRadius * (1 - math.cos(angleOut))
tangent = self.angleToVector(angleTangent) * tangentLength
normal = (
self.angleToVector(angleTangent + self.getLeadDir(obj, obj.InvertOut))
* normalLength
)
arcEnd = end + tangent + normal
lead.append(self.createArcMove(obj, end, arcEnd, normalMax, obj.InvertOut))
# append "Line" style lead-out
# Line3d the same as Line, but increased Z start point
elif obj.StyleOut in ("Line", "Line3d", "Perpendicular", "Tangent"):
# tangent and normal vectors in XY plane
tangentLength = math.cos(angleOut) * length
normalLength = math.sin(angleOut) * length
tangent = self.angleToVector(angleTangent) * tangentLength
normal = (
self.angleToVector(angleTangent + self.getLeadDir(obj, obj.InvertOut))
* normalLength
)
lineEnd = end + tangent + normal
lead.append(self.createStraightMove(obj, end, lineEnd))
# append "LineZ" style lead-out - vertical inclined line
# Should be apply only on straight Path segment
elif obj.StyleOut == "LineZ":
# tangent vector in XY plane
# normal vector is vertical
normalLengthMax = self.baseOp.StartDepth.Value - end.z
normalLength = math.sin(angleOut) * length
# do not exceed Normal vector max length
normalLength = min(normalLength, normalLengthMax)
tangentLength = normalLength / math.tan(angleOut)
tangent = self.angleToVector(angleTangent) * tangentLength
normal = App.Vector(0, 0, normalLength)
lineEnd = end + tangent + normal
lead.append(self.createStraightMove(obj, end, lineEnd))
# prepend "ArcZ" style lead-out - vertical Arc
# Should be apply only on straight Path segment
elif obj.StyleOut == "ArcZ":
# tangent vector in XY plane
# normal vector is vertical
arcRadius = length
normalLengthMax = self.baseOp.SafeHeight.Value - end.z
normalLength = arcRadius * (1 - math.cos(angleOut))
if normalLength > normalLengthMax:
# do not exceed Normal vector max length
normalLength = normalLengthMax
# recalculate angle for limited normal length
angleOut = math.acos((arcRadius - normalLength) / arcRadius)
tangentLength = arcRadius * math.sin(angleOut)
tangent = self.angleToVector(angleTangent) * tangentLength
normal = App.Vector(0, 0, normalLength)
arcEnd = end + tangent + normal
lead.extend(self.createArcZMoveUp(obj, end, arcEnd, arcRadius))
if obj.StyleOut in ("Arc3d", "Line3d"):
# Up Z end point for Arc3d and Line3d
if outInstrPrev and outInstrPrev.positionBegin().z > end.z:
lead[-1].param["Z"] = outInstrPrev.positionBegin().z
else:
lead[-1].param["Z"] = self.baseOp.StartDepth.Value
# append travel moves to clearance height after finish all profiles
if last and obj.StyleOut != "No Retract":
lead += self.getTravelEnd(obj)
return lead
# Check command
def isCuttingMove(self, instr):
result = instr.isMove() and not instr.isRapid() and not instr.isPlunge()
return result
# Get direction of non cut movements
def getMoveDir(self, instr):
if instr.positionBegin().z > instr.positionEnd().z:
return "Down"
elif instr.positionBegin().z < instr.positionEnd().z:
return "Up"
elif instr.pathLength() != 0:
return "Hor"
else:
# move command without change position
return None
# Get last index of mill command in whole Path
def findLastCuttingMoveIndex(self, source):
for i in range(len(source) - 1, -1, -1):
if self.isCuttingMove(source[i]):
return i
return None
# Get finish index of mill command for one profile
def findLastCutMultiProfileIndex(self, source, startIndex):
if startIndex >= len(source):
return len(source) - 1
for i in range(startIndex, len(source), +1):
if not self.isCuttingMove(source[i]):
return i - 1
return i
# Increase travel length from begin
def getOvertravelIn(self, obj, source, length, start, end):
startPoint = source[start].positionBegin()
endPoint = source[end].positionEnd()
if Path.Geom.pointsCoincide(startPoint, endPoint):
# closed profile
# get extra commands from end of the closed profile
measuredLength = 0
for i, instr in enumerate(reversed(source[start : end + 1])):
instrLength = instr.pathLength()
if Path.Geom.isRoughly(measuredLength + instrLength, length, 1):
# get needed length and not need to cut last command
commands = source[end - i : end + 1]
return commands
elif measuredLength + instrLength > length:
# measured length exceed needed length and need cut command
commands = source[end - i + 1 : end + 1]
newLength = length - measuredLength
newInstr = self.cutInstrBegin(obj, instr, newLength)
commands.insert(0, newInstr)
return commands
measuredLength += instrLength
else:
# open profile
# extend first move
instr = source[start]
newLength = length + instr.pathLength()
newInstr = self.cutInstrBegin(obj, instr, newLength)
return [newInstr]
return None
# Increase travel length from end
def getOvertravelOut(self, obj, source, length, start, end):
startPoint = source[start].positionBegin()
endPoint = source[end].positionEnd()
if Path.Geom.pointsCoincide(startPoint, endPoint):
# closed profile
# get extra commands from begin of the closed profile
measuredLength = 0
for i, instr in enumerate(source[start:end]):
instrLength = instr.pathLength()
if Path.Geom.isRoughly(measuredLength + instrLength, length, 1):
# get needed length and not need to cut last command
commands = source[start : start + i + 1]
return commands
elif measuredLength + instrLength > length:
# measured length exceed needed length and need cut command
commands = source[start : start + i]
newLength = length - measuredLength
newInstr = self.cutInstrEnd(obj, instr, newLength)
commands.append(newInstr)
return commands
measuredLength += instrLength
else:
# open profile
# extend last move
instr = source[end]
newLength = length + instr.pathLength()
newInstr = self.cutInstrEnd(obj, instr, newLength)
return [newInstr]
return None
# Cut travel end by distance (negative overtravel out)
def cutTravelEnd(self, obj, source, cutLength):
measuredLength = 0
for i, instr in enumerate(reversed(source)):
instrLength = instr.pathLength()
measuredLength += instrLength
if Path.Geom.isRoughly(measuredLength, cutLength):
# get needed length and not need to cut any command
commands = source[: -i - 1]
return commands
elif measuredLength > cutLength:
# measured length exceed needed cut length and need cut command
commands = source[: -i - 1]
newLength = measuredLength - cutLength
newInstr = self.cutInstrEnd(obj, instr, newLength)
commands.append(newInstr)
return commands
return None
# Change end point of instruction
def cutInstrEnd(self, obj, instr, newLength):
command = None
# Cut straight move from begin
if instr.isStraight():
begin = instr.positionBegin()
end = instr.positionEnd()
v = end - begin
n = math.hypot(v.x, v.y)
u = v / n
cutEnd = begin + u * newLength
command = self.createStraightMove(obj, begin, cutEnd)
# Cut arc move from begin
elif instr.isArc():
angleTangent = instr.anglesOfTangents()[0]
arcBegin = instr.positionBegin()
arcOffset = App.Vector(instr.i(), instr.j(), instr.k())
arcRadius = instr.arcRadius()
arcAngle = newLength / arcRadius
tangentLength = math.sin(arcAngle) * arcRadius
normalLength = arcRadius * (1 - math.cos(arcAngle))
tangent = self.angleToVector(angleTangent) * tangentLength
normal = self.angleToVector(angleTangent + self.getPathDir(obj)) * normalLength
arcEnd = arcBegin + tangent + normal
cmdName = "G2" if instr.isCW() else "G3"
command = self.createArcMoveN(obj, arcBegin, arcEnd, arcOffset, cmdName)
return command
# Change start point of instruction
def cutInstrBegin(self, obj, instr, newLength):
# Cut straignt move from begin
if instr.isStraight():
begin = instr.positionBegin()
end = instr.positionEnd()
v = end - begin
n = math.hypot(v.x, v.y)
u = v / n
newBegin = end - u * newLength
command = self.createStraightMove(obj, newBegin, end)
return command
# Cut arc move from begin
elif instr.isArc():
angleTangent = instr.anglesOfTangents()[1]
arcEnd = instr.positionEnd()
arcCenter = instr.xyCenter()
arcRadius = instr.arcRadius()
arcAngle = newLength / arcRadius
tangentLength = math.sin(arcAngle) * arcRadius
normalLength = arcRadius * (1 - math.cos(arcAngle))
tangent = -self.angleToVector(angleTangent) * tangentLength
normal = self.angleToVector(angleTangent + self.getPathDir(obj)) * normalLength
arcBegin = arcEnd + tangent + normal
arcOffset = arcCenter - arcBegin
cmdName = "G2" if instr.isCW() else "G3"
command = self.createArcMoveN(obj, arcBegin, arcEnd, arcOffset, cmdName)
return command
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 # prepare first move at clearance height
firstMillIndex = None # Index start mill instruction for one profile
lastCuttingMoveIndex = self.findLastCuttingMoveIndex(source)
inInstrPrev = None
outInstrPrev = None
measuredLength = 0 # for negative OffsetIn
skipCounter = 0 # for negative OffsetIn
commands = []
moveDir = None
# Process all instructions
for i, instr in enumerate(source):
# Process not mill instruction
if not self.isCuttingMove(instr):
if not instr.isMove():
# non-move instruction get added verbatim
commands.append(instr)
else:
moveDir = self.getMoveDir(instr)
if (not obj.LeadIn and not obj.LeadOut) and (
moveDir in ("Down", "Hor") or first
):
# keep original Lead-in movements
commands.append(instr)
if not obj.LeadOut and moveDir == "Up" and not first:
# keep original Lead-out movements
commands.append(instr)
# skip travel and plunge moves if LeadInOut will be process
# travel moves will be added in getLeadStart and getLeadEnd
continue
# measuring length for one profile
if self.isCuttingMove(instr):
measuredLength += instr.pathLength()
# Process Lead-In
if first or not self.isCuttingMove(source[i - 1 - skipCounter]):
if obj.LeadIn or obj.LeadOut:
# can not skip leadin if leadout
# Process negative Offset Lead-In (cut travel from begin)
if obj.OffsetIn.Value < 0 and obj.StyleIn != "No Retract":
if measuredLength <= abs(obj.OffsetIn.Value):
# skip mill instruction
skipCounter += 1 # count skipped instructions
continue
else:
skipCounter = 0
# cut mill instruction
newLength = measuredLength - abs(obj.OffsetIn.Value)
instr = self.cutInstrBegin(obj, instr, newLength)
# Process positive offset Lead-In (overtravel)
firstMillIndex = i
lastMillIndex = self.findLastCutMultiProfileIndex(source, i + 1)
overtravelIn = None
if obj.OffsetIn.Value > 0 and obj.StyleIn != "No Retract":
overtravelIn = self.getOvertravelIn(
obj,
source,
obj.OffsetIn.Value,
firstMillIndex,
lastMillIndex,
)
if overtravelIn:
commands.extend(
self.getLeadStart(
obj, overtravelIn[0], first, inInstrPrev, outInstrPrev
)
)
commands.extend(overtravelIn)
else:
commands.extend(
self.getLeadStart(obj, instr, first, inInstrPrev, outInstrPrev)
)
firstMillIndex = i if not firstMillIndex else firstMillIndex
inInstrPrev = commands[-1]
first = False
# Add mill instruction
commands.append(instr)
# Process Lead-Out
last = bool(i == lastCuttingMoveIndex)
if (last or not self.isCuttingMove(source[i + 1])) and obj.LeadOut:
measuredLength = 0 # reset measured length for last profile
lastMillIndex = i # index last mill instruction for last profile
# Process negative Offset Lead-Out (cut travel from end)
if obj.OffsetOut.Value < 0 and obj.StyleOut != "No Retract":
commands = self.cutTravelEnd(obj, commands, abs(obj.OffsetOut.Value))
# Process positive Offset Lead-Out (overtravel)
if obj.OffsetOut.Value > 0 and obj.StyleOut != "No Retract":
overtravelOut = self.getOvertravelOut(
obj,
source,
obj.OffsetOut.Value,
firstMillIndex,
lastMillIndex,
)
firstMillIndex = None
if overtravelOut:
commands.extend(overtravelOut)
# add lead end and travel moves
leadEndInstr = self.getLeadEnd(obj, commands[-1], last, outInstrPrev)
commands.extend(leadEndInstr)
# Last mill position to check RetractThreshold
if leadEndInstr:
outInstrPrev = leadEndInstr[-1]
else:
outInstrPrev = instr
maneuver.addInstructions(commands)
return maneuver.toPath()
class TaskDressupLeadInOut(SimpleEditPanel):
_transaction_name = "Edit LeadInOut Dress-up"
_ui_file = ":/panels/DressUpLeadInOutEdit.ui"
def setupUi(self):
self.connectWidget("InvertIn", self.form.chkInvertDirectionIn)
self.connectWidget("InvertOut", self.form.chkInvertDirectionOut)
self.connectWidget("RadiusIn", self.form.dspRadiusIn)
self.connectWidget("RadiusOut", self.form.dspRadiusOut)
self.connectWidget("StyleIn", self.form.cboStyleIn)
self.connectWidget("StyleOut", self.form.cboStyleOut)
self.connectWidget("AngleIn", self.form.dspAngleIn)
self.connectWidget("AngleOut", self.form.dspAngleOut)
self.connectWidget("OffsetIn", self.form.dspOffsetIn)
self.connectWidget("OffsetOut", self.form.dspOffsetOut)
self.connectWidget("RapidPlunge", self.form.chkRapidPlunge)
self.connectWidget("RetractThreshold", self.form.dspRetractThreshold)
self.setFields()
def handleGroupBoxCheck():
self.obj.LeadIn = self.form.groupBoxIn.isChecked()
self.obj.LeadOut = self.form.groupBoxOut.isChecked()
self.form.groupBoxIn.setChecked(self.obj.LeadIn)
self.form.groupBoxOut.setChecked(self.obj.LeadOut)
self.form.groupBoxIn.clicked.connect(handleGroupBoxCheck)
self.form.groupBoxOut.clicked.connect(handleGroupBoxCheck)
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", "Lead In/Out"),
"ToolTip": QT_TRANSLATE_NOOP(
"CAM_DressupLeadInOut",
"Creates entry and exit motions for a selected path",
),
}
def IsActive(self):
selection = FreeCADGui.Selection.getSelection()
if len(selection) != 1:
return False
if not selection[0].isDerivedFrom("Path::Feature"):
return False
if selection.Name.startswith("Job"):
return False
return True
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", "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", "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")