1434 lines
57 KiB
Python
1434 lines
57 KiB
Python
# SPDX-License-Identifier: LGPL-2.1-or-later
|
|
|
|
# ***************************************************************************
|
|
# * 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
|
|
import Path.Base.Gui.Util as PathGuiUtil
|
|
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"),
|
|
QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "Perpendicular"),
|
|
QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "Tangent"),
|
|
# 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", "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 onChanged(self, obj, prop):
|
|
if prop == "Path" and obj.ViewObject:
|
|
obj.ViewObject.signalChangeIcon()
|
|
|
|
def setup(self, obj):
|
|
obj.LeadIn = True
|
|
obj.LeadOut = True
|
|
obj.AngleIn = 90
|
|
obj.AngleOut = 90
|
|
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
|
|
|
|
# Use shared hideModes from TaskDressupLeadInOut
|
|
for k, v in TaskDressupLeadInOut.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
|
|
self.entranceFeed = self.toolController.LeadInFeed.Value
|
|
self.exitFeed = self.toolController.LeadOutFeed.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")
|
|
# Set previous value if possible
|
|
if styleOn in lead_styles:
|
|
obj.StyleIn = styleOn
|
|
elif styleOn == "Arc":
|
|
obj.StyleIn = "Arc"
|
|
obj.AngleIn = 90
|
|
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")
|
|
# Set previous value if possible
|
|
if styleOff in lead_styles:
|
|
obj.StyleOut = styleOff
|
|
elif styleOff == "Arc":
|
|
obj.StyleOut = "Arc"
|
|
obj.AngleOut = 90
|
|
|
|
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 = 90
|
|
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 = 90
|
|
|
|
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")
|
|
|
|
# Ensure correct initial visibility of fields after defaults are set
|
|
for k, v in TaskDressupLeadInOut.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)
|
|
|
|
# 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, feedRate):
|
|
param = {
|
|
"X": end.x,
|
|
"Y": end.y,
|
|
"Z": end.z,
|
|
"I": offset.x,
|
|
"J": offset.y,
|
|
"F": feedRate,
|
|
}
|
|
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, feedRate):
|
|
param = {"X": end.x, "Y": end.y, "I": offset.x, "J": offset.y, "F": feedRate}
|
|
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, feedRate):
|
|
param = {"X": end.x, "Y": end.y, "Z": end.z, "F": feedRate}
|
|
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, feedRate):
|
|
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": feedRate}
|
|
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, feedRate):
|
|
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": feedRate}
|
|
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, self.entranceFeed
|
|
)
|
|
)
|
|
|
|
# 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, self.entranceFeed))
|
|
|
|
# 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, self.entranceFeed))
|
|
|
|
# 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, self.entranceFeed)
|
|
)
|
|
|
|
# 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.entranceFeed}
|
|
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, self.exitFeed)
|
|
)
|
|
|
|
# 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, self.exitFeed))
|
|
|
|
# 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, self.exitFeed))
|
|
|
|
# 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, self.exitFeed))
|
|
|
|
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, self.horizFeed)
|
|
|
|
# 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, self.horizFeed)
|
|
|
|
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, self.horizFeed)
|
|
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, self.horizFeed)
|
|
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.setupSpinBoxes()
|
|
self.setupGroupBoxes()
|
|
self.setupDynamicVisibility()
|
|
self.setFields()
|
|
self.pageRegisterSignalHandlers()
|
|
|
|
def setupSpinBoxes(self):
|
|
self.connectWidget("InvertIn", self.form.chkInvertDirectionIn)
|
|
self.connectWidget("InvertOut", self.form.chkInvertDirectionOut)
|
|
self.connectWidget("PercentageRadiusIn", self.form.dspPercentageRadiusIn)
|
|
self.connectWidget("PercentageRadiusOut", self.form.dspPercentageRadiusOut)
|
|
self.connectWidget("StyleIn", self.form.cboStyleIn)
|
|
self.connectWidget("StyleOut", self.form.cboStyleOut)
|
|
self.radiusIn = PathGuiUtil.QuantitySpinBox(self.form.dspRadiusIn, self.obj, "RadiusIn")
|
|
self.radiusOut = PathGuiUtil.QuantitySpinBox(self.form.dspRadiusOut, self.obj, "RadiusOut")
|
|
self.angleIn = PathGuiUtil.QuantitySpinBox(self.form.dspAngleIn, self.obj, "AngleIn")
|
|
self.angleOut = PathGuiUtil.QuantitySpinBox(self.form.dspAngleOut, self.obj, "AngleOut")
|
|
self.offsetIn = PathGuiUtil.QuantitySpinBox(self.form.dspOffsetIn, self.obj, "OffsetIn")
|
|
self.offsetOut = PathGuiUtil.QuantitySpinBox(self.form.dspOffsetOut, self.obj, "OffsetOut")
|
|
self.connectWidget("RapidPlunge", self.form.chkRapidPlunge)
|
|
self.retractThreshold = PathGuiUtil.QuantitySpinBox(
|
|
self.form.dspRetractThreshold, self.obj, "RetractThreshold"
|
|
)
|
|
|
|
self.radiusIn.updateWidget()
|
|
self.radiusOut.updateWidget()
|
|
self.angleIn.updateWidget()
|
|
self.angleOut.updateWidget()
|
|
self.offsetIn.updateWidget()
|
|
self.offsetOut.updateWidget()
|
|
self.retractThreshold.updateWidget()
|
|
|
|
def setupGroupBoxes(self):
|
|
self.form.groupBoxIn.setChecked(self.obj.LeadIn)
|
|
self.form.groupBoxOut.setChecked(self.obj.LeadOut)
|
|
self.form.groupBoxIn.clicked.connect(self.handleGroupBoxCheck)
|
|
self.form.groupBoxOut.clicked.connect(self.handleGroupBoxCheck)
|
|
|
|
def handleGroupBoxCheck(self):
|
|
self.obj.LeadIn = self.form.groupBoxIn.isChecked()
|
|
self.obj.LeadOut = self.form.groupBoxOut.isChecked()
|
|
|
|
def setupDynamicVisibility(self):
|
|
self.form.cboStyleIn.currentIndexChanged.connect(self.updateLeadInVisibility)
|
|
self.form.cboStyleOut.currentIndexChanged.connect(self.updateLeadOutVisibility)
|
|
self.updateLeadInVisibility()
|
|
self.updateLeadOutVisibility()
|
|
|
|
def getSignalsForUpdate(self):
|
|
signals = []
|
|
signals.append(self.form.dspRadiusIn.editingFinished)
|
|
signals.append(self.form.dspRadiusOut.editingFinished)
|
|
signals.append(self.form.dspAngleIn.editingFinished)
|
|
signals.append(self.form.dspAngleOut.editingFinished)
|
|
signals.append(self.form.dspOffsetIn.editingFinished)
|
|
signals.append(self.form.dspOffsetOut.editingFinished)
|
|
signals.append(self.form.dspRetractThreshold.editingFinished)
|
|
return signals
|
|
|
|
def pageGetFields(self):
|
|
PathGuiUtil.updateInputField(self.obj, "RadiusIn", self.form.dspRadiusIn)
|
|
PathGuiUtil.updateInputField(self.obj, "RadiusOut", self.form.dspRadiusOut)
|
|
PathGuiUtil.updateInputField(self.obj, "AngleIn", self.form.dspAngleIn)
|
|
PathGuiUtil.updateInputField(self.obj, "AngleOut", self.form.dspAngleOut)
|
|
PathGuiUtil.updateInputField(self.obj, "OffsetIn", self.form.dspOffsetIn)
|
|
PathGuiUtil.updateInputField(self.obj, "OffsetOut", self.form.dspOffsetOut)
|
|
PathGuiUtil.updateInputField(self.obj, "RetractThreshold", self.form.dspRetractThreshold)
|
|
|
|
def pageRegisterSignalHandlers(self):
|
|
for signal in self.getSignalsForUpdate():
|
|
signal.connect(self.pageGetFields)
|
|
|
|
# Shared hideModes for both LeadIn and LeadOut
|
|
hideModes = {
|
|
"Angle": ("No Retract", "Perpendicular", "Tangent", "Vertical"),
|
|
"Invert": ("No Retract", "ArcZ", "LineZ", "Vertical", "Perpendicular", "Tangent"),
|
|
"Offset": ("No Retract"),
|
|
"Radius": ("No Retract", "Vertical"),
|
|
}
|
|
|
|
def updateLeadVisibility(self, style, angleWidget, invertWidget, angleLabel, radiusLabel=None):
|
|
# Dynamic label for Radius/Length
|
|
arc_styles = ("Arc", "Arc3d", "ArcZ", "Helix")
|
|
if radiusLabel and hasattr(self.form, radiusLabel):
|
|
if style in arc_styles:
|
|
getattr(self.form, radiusLabel).setText("Radius")
|
|
# Will do translation later
|
|
# getattr(self.form, radiusLabel).setText(translate("CAM_DressupLeadInOut", "Radius"))
|
|
else:
|
|
getattr(self.form, radiusLabel).setText("Length")
|
|
# Will do translation later
|
|
# getattr(self.form, radiusLabel).setText(translate("CAM_DressupLeadInOut", "Length"))
|
|
|
|
# Angle
|
|
if style in self.hideModes["Angle"]:
|
|
angleWidget.hide()
|
|
if hasattr(self.form, angleLabel):
|
|
getattr(self.form, angleLabel).hide()
|
|
else:
|
|
angleWidget.show()
|
|
if hasattr(self.form, angleLabel):
|
|
getattr(self.form, angleLabel).show()
|
|
# Invert Direction
|
|
if style in self.hideModes["Invert"]:
|
|
invertWidget.hide()
|
|
else:
|
|
invertWidget.show()
|
|
|
|
def updateLeadInVisibility(self):
|
|
style = self.form.cboStyleIn.currentText()
|
|
self.updateLeadVisibility(
|
|
style, self.form.dspAngleIn, self.form.chkInvertDirectionIn, "label_1", "label_5"
|
|
)
|
|
|
|
def updateLeadOutVisibility(self):
|
|
style = self.form.cboStyleOut.currentText()
|
|
self.updateLeadVisibility(
|
|
style, self.form.dspAngleOut, self.form.chkInvertDirectionOut, "label_11", "label_15"
|
|
)
|
|
|
|
|
|
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
|
|
|
|
def getIcon(self):
|
|
if getattr(PathDressup.baseOp(self.obj), "Active", True):
|
|
return ":/icons/CAM_Dressup.svg"
|
|
else:
|
|
return ":/icons/CAM_OpActive.svg"
|
|
|
|
|
|
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[0].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")
|