merge with formatting and translation changes

This commit is contained in:
jim
2022-01-05 15:01:43 -08:00
77 changed files with 5160 additions and 2643 deletions

View File

@@ -28,47 +28,121 @@ from PathScripts import PathLog
from PySide import QtCore
import math
import random
from PySide.QtCore import QT_TRANSLATE_NOOP
__doc__ = """Path Array object and FreeCAD command"""
# Qt translation handling
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
translate = FreeCAD.Qt.translate
class ObjectArray:
def __init__(self, obj):
obj.addProperty("App::PropertyLinkList", "Base",
"Path", QtCore.QT_TRANSLATE_NOOP("App::Property","The path(s) to array"))
obj.addProperty("App::PropertyEnumeration", "Type",
"Path", QtCore.QT_TRANSLATE_NOOP("App::Property", "Pattern method"))
obj.addProperty("App::PropertyVectorDistance", "Offset",
"Path", QtCore.QT_TRANSLATE_NOOP("App::Property","The spacing between the array copies in Linear pattern"))
obj.addProperty("App::PropertyInteger", "CopiesX",
"Path", QtCore.QT_TRANSLATE_NOOP("App::Property","The number of copies in X direction in Linear pattern"))
obj.addProperty("App::PropertyInteger", "CopiesY",
"Path", QtCore.QT_TRANSLATE_NOOP("App::Property","The number of copies in Y direction in Linear pattern"))
obj.addProperty("App::PropertyAngle", "Angle",
"Path", QtCore.QT_TRANSLATE_NOOP("App::Property","Total angle in Polar pattern"))
obj.addProperty("App::PropertyInteger", "Copies",
"Path", QtCore.QT_TRANSLATE_NOOP("App::Property","The number of copies in Linear 1D and Polar pattern"))
obj.addProperty("App::PropertyVector", "Centre",
"Path", QtCore.QT_TRANSLATE_NOOP("App::Property","The centre of rotation in Polar pattern"))
obj.addProperty("App::PropertyBool", "SwapDirection",
"Path", QtCore.QT_TRANSLATE_NOOP("App::Property","Make copies in X direction before Y in Linear 2D pattern"))
obj.addProperty("App::PropertyInteger", "JitterPercent",
"Path", QtCore.QT_TRANSLATE_NOOP("App::Property","Percent of copies to randomly offset"))
obj.addProperty("App::PropertyVectorDistance", "JitterMagnitude",
"Path", QtCore.QT_TRANSLATE_NOOP("App::Property","Maximum random offset of copies"))
obj.addProperty("App::PropertyInteger", "JitterSeed",
"Path", QtCore.QT_TRANSLATE_NOOP("App::Property","Seed value for jitter randomness"))
obj.addProperty("App::PropertyLink", "ToolController",
"Path", QtCore.QT_TRANSLATE_NOOP("App::Property", "The tool controller that will be used to calculate the path"))
obj.addProperty("App::PropertyBool", "Active",
"Path", QtCore.QT_TRANSLATE_NOOP("PathOp", "Make False, to prevent operation from generating code"))
obj.addProperty(
"App::PropertyLinkList",
"Base",
"Path",
QT_TRANSLATE_NOOP("App::Property", "The path(s) to array"),
)
obj.addProperty(
"App::PropertyEnumeration",
"Type",
"Path",
QT_TRANSLATE_NOOP("App::Property", "Pattern method"),
)
obj.addProperty(
"App::PropertyVectorDistance",
"Offset",
"Path",
QT_TRANSLATE_NOOP(
"App::Property",
"The spacing between the array copies in Linear pattern",
),
)
obj.addProperty(
"App::PropertyInteger",
"CopiesX",
"Path",
QT_TRANSLATE_NOOP(
"App::Property", "The number of copies in X direction in Linear pattern"
),
)
obj.addProperty(
"App::PropertyInteger",
"CopiesY",
"Path",
QT_TRANSLATE_NOOP(
"App::Property", "The number of copies in Y direction in Linear pattern"
),
)
obj.addProperty(
"App::PropertyAngle",
"Angle",
"Path",
QT_TRANSLATE_NOOP("App::Property", "Total angle in Polar pattern"),
)
obj.addProperty(
"App::PropertyInteger",
"Copies",
"Path",
QT_TRANSLATE_NOOP(
"App::Property", "The number of copies in Linear 1D and Polar pattern"
),
)
obj.addProperty(
"App::PropertyVector",
"Centre",
"Path",
QT_TRANSLATE_NOOP(
"App::Property", "The centre of rotation in Polar pattern"
),
)
obj.addProperty(
"App::PropertyBool",
"SwapDirection",
"Path",
QT_TRANSLATE_NOOP(
"App::Property",
"Make copies in X direction before Y in Linear 2D pattern",
),
)
obj.addProperty(
"App::PropertyInteger",
"JitterPercent",
"Path",
QT_TRANSLATE_NOOP("App::Property", "Percent of copies to randomly offset"),
)
obj.addProperty(
"App::PropertyVectorDistance",
"JitterMagnitude",
"Path",
QT_TRANSLATE_NOOP("App::Property", "Maximum random offset of copies"),
)
obj.addProperty(
"App::PropertyInteger",
"JitterSeed",
"Path",
QT_TRANSLATE_NOOP("App::Property","Seed value for jitter randomness"),
)
obj.addProperty(
"App::PropertyLink",
"ToolController",
"Path",
QT_TRANSLATE_NOOP(
"App::Property",
"The tool controller that will be used to calculate the path",
),
)
obj.addProperty(
"App::PropertyBool",
"Active",
"Path",
QT_TRANSLATE_NOOP(
"PathOp", "Make False, to prevent operation from generating code"
),
)
obj.Active = True
obj.Type = ['Linear1D', 'Linear2D', 'Polar']
obj.Type = ["Linear1D", "Linear2D", "Polar"]
self.setEditorModes(obj)
obj.Proxy = self
@@ -80,13 +154,13 @@ class ObjectArray:
return None
def setEditorModes(self, obj):
if obj.Type == 'Linear1D':
if obj.Type == "Linear1D":
angleMode = centreMode = copiesXMode = copiesYMode = swapDirectionMode = 2
copiesMode = offsetMode = 0
elif obj.Type == 'Linear2D':
elif obj.Type == "Linear2D":
angleMode = copiesMode = centreMode = 2
copiesXMode = copiesYMode = offsetMode = swapDirectionMode = 0
elif obj.Type == 'Polar':
elif obj.Type == "Polar":
angleMode = copiesMode = centreMode = 0
copiesXMode = copiesYMode = offsetMode = swapDirectionMode = 2
@@ -95,17 +169,17 @@ class ObjectArray:
"Path", QtCore.QT_TRANSLATE_NOOP("App::Property","Seed value for jitter randomness"))
obj.JitterSeed = 0
obj.setEditorMode('Angle', angleMode)
obj.setEditorMode('Copies', copiesMode)
obj.setEditorMode('Centre', centreMode)
obj.setEditorMode('CopiesX', copiesXMode)
obj.setEditorMode('CopiesY', copiesYMode)
obj.setEditorMode('Offset', offsetMode)
obj.setEditorMode('SwapDirection', swapDirectionMode)
obj.setEditorMode('JitterPercent', 0)
obj.setEditorMode('JitterMagnitude', 0)
obj.setEditorMode('JitterSeed', 0)
obj.setEditorMode('ToolController', 2)
obj.setEditorMode("Angle", angleMode)
obj.setEditorMode("Copies", copiesMode)
obj.setEditorMode("Centre", centreMode)
obj.setEditorMode("CopiesX", copiesXMode)
obj.setEditorMode("CopiesY", copiesYMode)
obj.setEditorMode("Offset", offsetMode)
obj.setEditorMode("SwapDirection", swapDirectionMode)
obj.setEditorMode("JitterPercent", 0)
obj.setEditorMode("JitterMagnitude", 0)
obj.setEditorMode("JitterSeed", 0)
obj.setEditorMode("ToolController", 2)
def onChanged(self, obj, prop):
if prop == "Type":
@@ -115,31 +189,41 @@ class ObjectArray:
"""onDocumentRestored(obj) ... Called automatically when document is restored."""
if not hasattr(obj, "Active"):
obj.addProperty("App::PropertyBool", "Active",
"Path", QtCore.QT_TRANSLATE_NOOP("PathOp", "Make False, to prevent operation from generating code"))
obj.addProperty(
"App::PropertyBool",
"Active",
"Path",
QT_TRANSLATE_NOOP(
"PathOp", "Make False, to prevent operation from generating code"
),
)
obj.Active = True
self.setEditorModes(obj)
def rotatePath(self, path, angle, centre):
'''
Rotates Path around given centre vector
Only X and Y is considered
'''
CmdMoveRapid = ['G0', 'G00']
CmdMoveStraight = ['G1', 'G01']
CmdMoveCW = ['G2', 'G02']
CmdMoveCCW = ['G3', 'G03']
CmdDrill = ['G81', 'G82', 'G83']
CmdMoveArc = CmdMoveCW + CmdMoveCCW
CmdMove = CmdMoveStraight + CmdMoveArc
"""
Rotates Path around given centre vector
Only X and Y is considered
"""
CmdMoveRapid = ["G0", "G00"]
CmdMoveStraight = ["G1", "G01"]
CmdMoveCW = ["G2", "G02"]
CmdMoveCCW = ["G3", "G03"]
CmdDrill = ["G81", "G82", "G83"]
CmdMoveArc = CmdMoveCW + CmdMoveCCW
CmdMove = CmdMoveStraight + CmdMoveArc
commands = []
ang = angle / 180 * math.pi
currX = 0
currY = 0
for cmd in path.Commands:
if (cmd.Name in CmdMoveRapid) or (cmd.Name in CmdMove) or (cmd.Name in CmdDrill):
if (
(cmd.Name in CmdMoveRapid)
or (cmd.Name in CmdMove)
or (cmd.Name in CmdDrill)
):
params = cmd.Parameters
x = params.get("X")
if x is None:
@@ -159,7 +243,7 @@ class ObjectArray:
ny = y * math.cos(ang) + x * math.sin(ang)
# "move" the centre back and update
params.update({'X': nx + centre.x, 'Y': ny + centre.y})
params.update({"X": nx + centre.x, "Y": ny + centre.y})
# Arcs need to have the I and J params rotated as well
if cmd.Name in CmdMoveArc:
@@ -172,7 +256,7 @@ class ObjectArray:
ni = i * math.cos(ang) - j * math.sin(ang)
nj = j * math.cos(ang) + i * math.sin(ang)
params.update({'I': ni, 'J': nj})
params.update({"I": ni, "J": nj})
cmd.Parameters = params
commands.append(cmd)
@@ -192,7 +276,7 @@ class ObjectArray:
obj.ToolController = base[0].ToolController
# Do not generate paths and clear current Path data if operation not
# Do not generate paths and clear current Path data if operation not
if not obj.Active:
if obj.Path:
obj.Path = Path.Path()
@@ -201,9 +285,21 @@ class ObjectArray:
# use seed if specified, otherwise default to object name for consistency during recomputes
seed = obj.JitterSeed or obj.Name
pa = PathArray(obj.Base, obj.Type, obj.Copies, obj.Offset,
obj.CopiesX, obj.CopiesY, obj.Angle, obj.Centre, obj.SwapDirection,
obj.JitterMagnitude, obj.JitterPercent, seed)
pa = PathArray(
obj.Base,
obj.Type,
obj.Copies,
obj.Offset,
obj.CopiesX,
obj.CopiesY,
obj.Angle,
obj.Centre,
obj.SwapDirection,
obj.JitterMagnitude,
obj.JitterPercent,
seed,
)
obj.Path = pa.getPath()
@@ -212,10 +308,21 @@ class PathArray:
This class receives one or more base operations and repeats those operations
at set intervals based upon array type requested and the related settings for that type."""
def __init__(self, baseList, arrayType, copies, offsetVector,
copiesX, copiesY, angle, centre, swapDirection,
jitterMagnitude=FreeCAD.Vector(0, 0, 0), jitterPercent=0,
seed='FreeCAD'):
def __init__(
self,
baseList,
arrayType,
copies,
offsetVector,
copiesX,
copiesY,
angle,
centre,
swapDirection,
jitterMagnitude=FreeCAD.Vector(0, 0, 0),
jitterPercent=0,
seed="FreeCAD",
):
self.baseList = list()
self.arrayType = arrayType # ['Linear1D', 'Linear2D', 'Polar']
self.copies = copies
@@ -241,10 +348,16 @@ class PathArray:
Returns the position argument with a random vector shift applied."""
if self.jitterPercent == 0:
pass
elif random.randint(0,100) < self.jitterPercent:
pos.x = pos.x + random.uniform(-self.jitterMagnitude.x, self.jitterMagnitude.x)
pos.y = pos.y + random.uniform(-self.jitterMagnitude.y, self.jitterMagnitude.y)
pos.z = pos.z + random.uniform(-self.jitterMagnitude.z, self.jitterMagnitude.z)
elif random.randint(0, 100) < self.jitterPercent:
pos.x = pos.x + random.uniform(
-self.jitterMagnitude.x, self.jitterMagnitude.x
)
pos.y = pos.y + random.uniform(
-self.jitterMagnitude.y, self.jitterMagnitude.y
)
pos.z = pos.z + random.uniform(
-self.jitterMagnitude.z, self.jitterMagnitude.z
)
return pos
# Public method
@@ -266,32 +379,48 @@ class PathArray:
return
if b.ToolController != base[0].ToolController:
# this may be important if Job output is split by tool controller
PathLog.warning(translate("PathArray", "Arrays of paths having different tool controllers are handled according to the tool controller of the first path."))
PathLog.warning(
translate(
"PathArray",
"Arrays of paths having different tool controllers are handled according to the tool controller of the first path.",
)
)
# build copies
output = ""
random.seed(self.seed)
if self.arrayType == 'Linear1D':
if self.arrayType == "Linear1D":
for i in range(self.copies):
pos = FreeCAD.Vector(self.offsetVector.x * (i + 1), self.offsetVector.y * (i + 1), self.offsetVector.z * (i + 1))
pos = FreeCAD.Vector(
self.offsetVector.x * (i + 1),
self.offsetVector.y * (i + 1),
self.offsetVector.z * (i + 1),
)
pos = self._calculateJitter(pos)
for b in base:
pl = FreeCAD.Placement()
pl.move(pos)
np = Path.Path([cm.transform(pl)
for cm in b.Path.Commands])
np = Path.Path([cm.transform(pl) for cm in b.Path.Commands])
output += np.toGCode()
elif self.arrayType == 'Linear2D':
elif self.arrayType == "Linear2D":
if self.swapDirection:
for i in range(self.copiesY + 1):
for j in range(self.copiesX + 1):
if (i % 2) == 0:
pos = FreeCAD.Vector(self.offsetVector.x * j, self.offsetVector.y * i, self.offsetVector.z * i)
pos = FreeCAD.Vector(
self.offsetVector.x * j,
self.offsetVector.y * i,
self.offsetVector.z * i,
)
else:
pos = FreeCAD.Vector(self.offsetVector.x * (self.copiesX - j), self.offsetVector.y * i, self.offsetVector.z * i)
pos = FreeCAD.Vector(
self.offsetVector.x * (self.copiesX - j),
self.offsetVector.y * i,
self.offsetVector.z * i,
)
pos = self._calculateJitter(pos)
for b in base:
@@ -299,15 +428,25 @@ class PathArray:
# do not process the index 0,0. It will be processed by the base Paths themselves
if not (i == 0 and j == 0):
pl.move(pos)
np = Path.Path([cm.transform(pl) for cm in b.Path.Commands])
np = Path.Path(
[cm.transform(pl) for cm in b.Path.Commands]
)
output += np.toGCode()
else:
for i in range(self.copiesX + 1):
for j in range(self.copiesY + 1):
if (i % 2) == 0:
pos = FreeCAD.Vector(self.offsetVector.x * i, self.offsetVector.y * j, self.offsetVector.z * i)
pos = FreeCAD.Vector(
self.offsetVector.x * i,
self.offsetVector.y * j,
self.offsetVector.z * i,
)
else:
pos = FreeCAD.Vector(self.offsetVector.x * i, self.offsetVector.y * (self.copiesY - j), self.offsetVector.z * i)
pos = FreeCAD.Vector(
self.offsetVector.x * i,
self.offsetVector.y * (self.copiesY - j),
self.offsetVector.z * i,
)
pos = self._calculateJitter(pos)
for b in base:
@@ -315,7 +454,9 @@ class PathArray:
# do not process the index 0,0. It will be processed by the base Paths themselves
if not (i == 0 and j == 0):
pl.move(pos)
np = Path.Path([cm.transform(pl) for cm in b.Path.Commands])
np = Path.Path(
[cm.transform(pl) for cm in b.Path.Commands]
)
output += np.toGCode()
# Eif
else:
@@ -332,7 +473,6 @@ class PathArray:
class ViewProviderArray:
def __init__(self, vobj):
self.Object = vobj.Object
vobj.Proxy = self
@@ -356,11 +496,14 @@ class ViewProviderArray:
class CommandPathArray:
def GetResources(self):
return {'Pixmap': 'Path_Array',
'MenuText': QtCore.QT_TRANSLATE_NOOP("Path_Array", "Array"),
'ToolTip': QtCore.QT_TRANSLATE_NOOP("Path_Array", "Creates an array from selected path(s)")}
return {
"Pixmap": "Path_Array",
"MenuText": QT_TRANSLATE_NOOP("Path_Array", "Array"),
"ToolTip": QT_TRANSLATE_NOOP(
"Path_Array", "Creates an array from selected path(s)"
),
}
def IsActive(self):
if bool(FreeCADGui.Selection.getSelection()) is False:
@@ -368,7 +511,7 @@ class CommandPathArray:
try:
obj = FreeCADGui.Selection.getSelectionEx()[0].Object
return isinstance(obj.Proxy, PathScripts.PathOp.ObjectOp)
except(IndexError, AttributeError):
except (IndexError, AttributeError):
return False
def Activated(self):
@@ -377,9 +520,13 @@ class CommandPathArray:
selection = FreeCADGui.Selection.getSelection()
for sel in selection:
if not(sel.isDerivedFrom("Path::Feature")):
if not (sel.isDerivedFrom("Path::Feature")):
FreeCAD.Console.PrintError(
translate("Path_Array", "Arrays can be created only from Path operations.")+"\n")
translate(
"Path_Array", "Arrays can be created only from Path operations."
)
+ "\n"
)
return
# if everything is ok, execute and register the transaction in the
@@ -388,19 +535,23 @@ class CommandPathArray:
FreeCADGui.addModule("PathScripts.PathArray")
FreeCADGui.addModule("PathScripts.PathUtils")
FreeCADGui.doCommand('obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython","Array")')
FreeCADGui.doCommand(
'obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython","Array")'
)
FreeCADGui.doCommand('PathScripts.PathArray.ObjectArray(obj)')
FreeCADGui.doCommand("PathScripts.PathArray.ObjectArray(obj)")
baseString = "[%s]" % ','.join(["FreeCAD.ActiveDocument.%s" % sel.Name for sel in selection])
FreeCADGui.doCommand('obj.Base = %s' % baseString)
baseString = "[%s]" % ",".join(
["FreeCAD.ActiveDocument.%s" % sel.Name for sel in selection]
)
FreeCADGui.doCommand("obj.Base = %s" % baseString)
FreeCADGui.doCommand('obj.ViewObject.Proxy = 0')
FreeCADGui.doCommand('PathScripts.PathUtils.addToJob(obj)')
FreeCADGui.doCommand("obj.ViewObject.Proxy = 0")
FreeCADGui.doCommand("PathScripts.PathUtils.addToJob(obj)")
FreeCAD.ActiveDocument.commitTransaction()
FreeCAD.ActiveDocument.recompute()
if FreeCAD.GuiUp:
# register the FreeCAD command
FreeCADGui.addCommand('Path_Array', CommandPathArray())
FreeCADGui.addCommand("Path_Array", CommandPathArray())

View File

@@ -24,16 +24,12 @@ import FreeCAD
import PathScripts.PathLog as PathLog
from PySide import QtCore
from PathScripts.PathUtils import waiting_effects
from PySide.QtCore import QT_TRANSLATE_NOOP
LOG_MODULE = 'PathCollision'
LOG_MODULE = "PathCollision"
PathLog.setLevel(PathLog.Level.DEBUG, LOG_MODULE)
PathLog.trackModule('PathCollision')
FreeCAD.setLogLevel('Path.Area', 0)
# Qt translation handling
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
PathLog.trackModule("PathCollision")
FreeCAD.setLogLevel("Path.Area", 0)
__title__ = "Path Collision Utility"
__author__ = "sliptonic (Brad Collette)"
@@ -44,19 +40,25 @@ __url__ = "https://www.freecadweb.org"
class _CollisionSim:
def __init__(self, obj):
#obj.addProperty("App::PropertyLink", "Original", "reference", QtCore.QT_TRANSLATE_NOOP("App::Property", "The base object this collision refers to"))
obj.Proxy = self
def execute(self, fp):
'''Do something when doing a recomputation, this method is mandatory'''
print('_CollisionSim', fp)
"""Do something when doing a recomputation, this method is mandatory"""
print("_CollisionSim", fp)
class _ViewProviderCollisionSim:
def __init__(self, vobj):
self.Object = vobj.Object
vobj.Proxy = self
vobj.addProperty("App::PropertyLink", "Original", "reference", QtCore.QT_TRANSLATE_NOOP("App::Property", "The base object this collision refers to"))
vobj.addProperty(
"App::PropertyLink",
"Original",
"reference",
QT_TRANSLATE_NOOP(
"App::Property", "The base object this collision refers to"
),
)
def attach(self, vobj):
self.Object = vobj.Object
@@ -82,12 +84,14 @@ class _ViewProviderCollisionSim:
def __compareBBSpace(bb1, bb2):
if (bb1.XMin == bb2.XMin and
bb1.XMax == bb2.XMax and
bb1.YMin == bb2.YMin and
bb1.YMax == bb2.YMax and
bb1.ZMin == bb2.ZMin and
bb1.ZMax == bb2.ZMax):
if (
bb1.XMin == bb2.XMin
and bb1.XMax == bb2.XMax
and bb1.YMin == bb2.YMin
and bb1.YMax == bb2.YMax
and bb1.ZMin == bb2.ZMin
and bb1.ZMax == bb2.ZMax
):
return True
return False
@@ -124,6 +128,3 @@ def getCollisionObject(baseobject, simobject):
obj.ViewObject.Original = baseobject
return result

View File

@@ -20,25 +20,29 @@
# * *
# ***************************************************************************
'''Used for CNC machine comments for Path module. Create a comment and place it in the Document tree.'''
"""Used for CNC machine comments for Path module. Create a comment and place it in the Document tree."""
import FreeCAD
import FreeCADGui
import Path
from PySide import QtCore
# Qt translation handling
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
from PySide.QtCore import QT_TRANSLATE_NOOP
translate = FreeCAD.Qt.translate
class Comment:
def __init__(self, obj):
obj.addProperty("App::PropertyString", "Comment",
"Path", "Comment or note for CNC program")
obj.addProperty(
"App::PropertyString",
"Comment",
"Path",
QT_TRANSLATE_NOOP("App::Property", "Comment or note for CNC program"),
)
obj.Proxy = self
mode = 2
obj.setEditorMode('Placement', mode)
obj.setEditorMode("Placement", mode)
def __getstate__(self):
return None
@@ -51,25 +55,24 @@ class Comment:
def execute(self, obj):
output = ""
output += '(' + str(obj.Comment) + ')\n'
output += "(" + str(obj.Comment) + ")\n"
path = Path.Path(output)
obj.Path = path
class _ViewProviderComment:
def __init__(self, vobj): # mandatory
vobj.Proxy = self
mode = 2
vobj.setEditorMode('LineWidth', mode)
vobj.setEditorMode('MarkerColor', mode)
vobj.setEditorMode('NormalColor', mode)
vobj.setEditorMode('DisplayMode', mode)
vobj.setEditorMode('BoundingBox', mode)
vobj.setEditorMode('Selectable', mode)
vobj.setEditorMode('ShapeColor', mode)
vobj.setEditorMode('Transparency', mode)
vobj.setEditorMode('Visibility', mode)
vobj.setEditorMode("LineWidth", mode)
vobj.setEditorMode("MarkerColor", mode)
vobj.setEditorMode("NormalColor", mode)
vobj.setEditorMode("DisplayMode", mode)
vobj.setEditorMode("BoundingBox", mode)
vobj.setEditorMode("Selectable", mode)
vobj.setEditorMode("ShapeColor", mode)
vobj.setEditorMode("Transparency", mode)
vobj.setEditorMode("Visibility", mode)
def __getstate__(self): # mandatory
return None
@@ -83,23 +86,26 @@ class _ViewProviderComment:
def onChanged(self, vobj, prop): # optional
# pylint: disable=unused-argument
mode = 2
vobj.setEditorMode('LineWidth', mode)
vobj.setEditorMode('MarkerColor', mode)
vobj.setEditorMode('NormalColor', mode)
vobj.setEditorMode('DisplayMode', mode)
vobj.setEditorMode('BoundingBox', mode)
vobj.setEditorMode('Selectable', mode)
vobj.setEditorMode('ShapeColor', mode)
vobj.setEditorMode('Transparency', mode)
vobj.setEditorMode('Visibility', mode)
vobj.setEditorMode("LineWidth", mode)
vobj.setEditorMode("MarkerColor", mode)
vobj.setEditorMode("NormalColor", mode)
vobj.setEditorMode("DisplayMode", mode)
vobj.setEditorMode("BoundingBox", mode)
vobj.setEditorMode("Selectable", mode)
vobj.setEditorMode("ShapeColor", mode)
vobj.setEditorMode("Transparency", mode)
vobj.setEditorMode("Visibility", mode)
class CommandPathComment:
def GetResources(self):
return {'Pixmap': 'Path_Comment',
'MenuText': QtCore.QT_TRANSLATE_NOOP("Path_Comment", "Comment"),
'ToolTip': QtCore.QT_TRANSLATE_NOOP("Path_Comment", "Add a Comment to your CNC program")}
return {
"Pixmap": "Path_Comment",
"MenuText": QT_TRANSLATE_NOOP("Path_Comment", "Comment"),
"ToolTip": QT_TRANSLATE_NOOP(
"Path_Comment", "Add a Comment to your CNC program"
),
}
def IsActive(self):
if FreeCAD.ActiveDocument is not None:
@@ -109,10 +115,9 @@ class CommandPathComment:
return False
def Activated(self):
FreeCAD.ActiveDocument.openTransaction(
translate("Path_Comment", "Create a Comment in your CNC program"))
FreeCAD.ActiveDocument.openTransaction("Create a Comment in your CNC program")
FreeCADGui.addModule("PathScripts.PathComment")
snippet = '''
snippet = """
import Path
import PathScripts
from PathScripts import PathUtils
@@ -121,14 +126,15 @@ PathScripts.PathComment.Comment(obj)
PathScripts.PathComment._ViewProviderComment(obj.ViewObject)
PathUtils.addToJob(obj)
'''
"""
FreeCADGui.doCommand(snippet)
FreeCAD.ActiveDocument.commitTransaction()
FreeCAD.ActiveDocument.recompute()
if FreeCAD.GuiUp:
# register the FreeCAD command
FreeCADGui.addCommand('Path_Comment', CommandPathComment())
FreeCADGui.addCommand("Path_Comment", CommandPathComment())
FreeCAD.Console.PrintLog("Loading PathComment... done\n")

View File

@@ -24,18 +24,31 @@ import FreeCAD
import FreeCADGui
from PySide import QtCore
from PySide.QtCore import QT_TRANSLATE_NOOP
__doc__ = """Path Copy object and FreeCAD command"""
# Qt translation handling
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
translate = FreeCAD.Qt.translate
class ObjectPathCopy:
def __init__(self, obj):
obj.addProperty("App::PropertyLink", "Base", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property", "The path to be copied"))
obj.addProperty("App::PropertyLink", "ToolController", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property", "The tool controller that will be used to calculate the path"))
obj.addProperty(
"App::PropertyLink",
"Base",
"Path",
QT_TRANSLATE_NOOP("App::Property", "The path to be copied"),
)
obj.addProperty(
"App::PropertyLink",
"ToolController",
"Path",
QT_TRANSLATE_NOOP(
"App::Property",
"The tool controller that will be used to calculate the path",
),
)
obj.Proxy = self
def __getstate__(self):
@@ -46,14 +59,13 @@ class ObjectPathCopy:
def execute(self, obj):
if obj.Base:
if hasattr(obj.Base, 'ToolController'):
if hasattr(obj.Base, "ToolController"):
obj.ToolController = obj.Base.ToolController
if obj.Base.Path:
obj.Path = obj.Base.Path.copy()
class ViewProviderPathCopy:
def __init__(self, vobj):
self.Object = vobj.Object
vobj.Proxy = self
@@ -73,11 +85,14 @@ class ViewProviderPathCopy:
class CommandPathCopy:
def GetResources(self):
return {'Pixmap': 'Path_Copy',
'MenuText': QtCore.QT_TRANSLATE_NOOP("Path_Copy", "Copy"),
'ToolTip': QtCore.QT_TRANSLATE_NOOP("Path_Copy", "Creates a linked copy of another path")}
return {
"Pixmap": "Path_Copy",
"MenuText": QT_TRANSLATE_NOOP("Path_Copy", "Copy"),
"ToolTip": QT_TRANSLATE_NOOP(
"Path_Copy", "Creates a linked copy of another path"
),
}
def IsActive(self):
if FreeCAD.ActiveDocument is not None:
@@ -88,11 +103,10 @@ class CommandPathCopy:
def Activated(self):
FreeCAD.ActiveDocument.openTransaction(
translate("Path_Copy", "Create Copy"))
FreeCAD.ActiveDocument.openTransaction("Create Copy")
FreeCADGui.addModule("PathScripts.PathCopy")
consolecode = '''
consolecode = """
import Path
import PathScripts
from PathScripts import PathCopy
@@ -123,7 +137,7 @@ proj.Group = g
FreeCAD.ActiveDocument.recompute()
'''
"""
FreeCADGui.doCommand(consolecode)
FreeCAD.ActiveDocument.commitTransaction()
@@ -132,6 +146,6 @@ FreeCAD.ActiveDocument.recompute()
if FreeCAD.GuiUp:
# register the FreeCAD command
FreeCADGui.addCommand('Path_Copy', CommandPathCopy())
FreeCADGui.addCommand("Path_Copy", CommandPathCopy())
FreeCAD.Console.PrintLog("Loading PathCopy... done\n")

View File

@@ -26,7 +26,7 @@ import Path
import PathScripts.PathOp as PathOp
import PathScripts.PathLog as PathLog
from PySide import QtCore
from PySide.QtCore import QT_TRANSLATE_NOOP
__title__ = "Path Custom Operation"
__author__ = "sliptonic (Brad Collette)"
@@ -34,13 +34,14 @@ __url__ = "http://www.freecadweb.org"
__doc__ = "Path Custom object and FreeCAD command"
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
# PathLog.trackModule(PathLog.thisModule())
if False:
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
PathLog.trackModule(PathLog.thisModule())
else:
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
# Qt translation handling
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
translate = FreeCAD.Qt.translate
class ObjectCustom(PathOp.ObjectOp):
@@ -48,8 +49,12 @@ class ObjectCustom(PathOp.ObjectOp):
return PathOp.FeatureTool | PathOp.FeatureCoolant
def initOperation(self, obj):
obj.addProperty("App::PropertyStringList", "Gcode", "Path",
QtCore.QT_TRANSLATE_NOOP("PathCustom", "The gcode to be inserted"))
obj.addProperty(
"App::PropertyStringList",
"Gcode",
"Path",
QT_TRANSLATE_NOOP("App::Property", "The gcode to be inserted"),
)
obj.Proxy = self
@@ -69,7 +74,7 @@ def SetupProperties():
def Create(name, obj=None, parentJob=None):
'''Create(name) ... Creates and returns a Custom operation.'''
"""Create(name) ... Creates and returns a Custom operation."""
if obj is None:
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
obj.Proxy = ObjectCustom(obj, name, parentJob)

View File

@@ -25,7 +25,8 @@ import FreeCADGui
import PathScripts.PathCustom as PathCustom
import PathScripts.PathOpGui as PathOpGui
from PySide import QtCore
from PySide.QtCore import QT_TRANSLATE_NOOP
__title__ = "Path Custom Operation UI"
__author__ = "sliptonic (Brad Collette)"
@@ -33,38 +34,26 @@ __url__ = "http://www.freecadweb.org"
__doc__ = "Custom operation page controller and command implementation."
# Qt translation handling
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
# class TaskPanelBaseGeometryPage(PathOpGui.TaskPanelBaseGeometryPage):
# '''Page controller for the base geometry.'''
# def getForm(self):
# return None
class TaskPanelOpPage(PathOpGui.TaskPanelPage):
'''Page controller class for the Custom operation.'''
"""Page controller class for the Custom operation."""
def getForm(self):
'''getForm() ... returns UI'''
"""getForm() ... returns UI"""
return FreeCADGui.PySideUic.loadUi(":/panels/PageOpCustomEdit.ui")
def getFields(self, obj):
'''getFields(obj) ... transfers values from UI to obj's properties'''
"""getFields(obj) ... transfers values from UI to obj's properties"""
self.updateToolController(obj, self.form.toolController)
self.updateCoolant(obj, self.form.coolantController)
def setFields(self, obj):
'''setFields(obj) ... transfers obj's property values to UI'''
"""setFields(obj) ... transfers obj's property values to UI"""
self.setupToolController(obj, self.form.toolController)
self.form.txtGCode.setText("\n".join(obj.Gcode))
self.setupCoolant(obj, self.form.coolantController)
def getSignalsForUpdate(self, obj):
'''getSignalsForUpdate(obj) ... return list of signals for updating obj'''
"""getSignalsForUpdate(obj) ... return list of signals for updating obj"""
signals = []
signals.append(self.form.toolController.currentIndexChanged)
signals.append(self.form.coolantController.currentIndexChanged)
@@ -75,10 +64,14 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
self.obj.Gcode = self.form.txtGCode.toPlainText().splitlines()
Command = PathOpGui.SetupOperation('Custom', PathCustom.Create, TaskPanelOpPage,
'Path_Custom',
QtCore.QT_TRANSLATE_NOOP("Path_Custom", "Custom"),
QtCore.QT_TRANSLATE_NOOP("Path_Custom", "Create custom gcode snippet"),
PathCustom.SetupProperties)
Command = PathOpGui.SetupOperation(
"Custom",
PathCustom.Create,
TaskPanelOpPage,
"Path_Custom",
QT_TRANSLATE_NOOP("Path_Custom", "Custom"),
QT_TRANSLATE_NOOP("Path_Custom", "Create custom gcode snippet"),
PathCustom.SetupProperties,
)
FreeCAD.Console.PrintLog("Loading PathCustomGui... done\n")

View File

@@ -27,10 +27,12 @@ from PySide import QtCore
import math
import PathScripts.PathUtils as PathUtils
import PathScripts.PathGui as PathGui
from PySide.QtCore import QT_TRANSLATE_NOOP
# lazily loaded modules
from lazy_loader.lazy_loader import LazyLoader
D = LazyLoader('DraftVecUtils', globals(), 'DraftVecUtils')
D = LazyLoader("DraftVecUtils", globals(), "DraftVecUtils")
__doc__ = """Dragknife Dressup object and FreeCAD command"""
@@ -38,25 +40,47 @@ if FreeCAD.GuiUp:
import FreeCADGui
# Qt translation handling
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
translate = FreeCAD.Qt.translate
movecommands = ['G1', 'G01', 'G2', 'G02', 'G3', 'G03']
rapidcommands = ['G0', 'G00']
arccommands = ['G2', 'G3', 'G02', 'G03']
movecommands = ["G1", "G01", "G2", "G02", "G3", "G03"]
rapidcommands = ["G0", "G00"]
arccommands = ["G2", "G3", "G02", "G03"]
currLocation = {}
class ObjectDressup:
def __init__(self, obj):
obj.addProperty("App::PropertyLink", "Base", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property", "The base path to modify"))
obj.addProperty("App::PropertyAngle", "filterAngle", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property", "Angles less than filter angle will not receive corner actions"))
obj.addProperty("App::PropertyFloat", "offset", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property", "Distance the point trails behind the spindle"))
obj.addProperty("App::PropertyFloat", "pivotheight", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property", "Height to raise during corner action"))
obj.addProperty(
"App::PropertyLink",
"Base",
"Path",
QT_TRANSLATE_NOOP("App::Property", "The base path to modify"),
)
obj.addProperty(
"App::PropertyAngle",
"filterAngle",
"Path",
QT_TRANSLATE_NOOP(
"App::Property",
"Angles less than filter angle will not receive corner actions",
),
)
obj.addProperty(
"App::PropertyFloat",
"offset",
"Path",
QT_TRANSLATE_NOOP(
"App::Property", "Distance the point trails behind the spindle"
),
)
obj.addProperty(
"App::PropertyFloat",
"pivotheight",
"Path",
QT_TRANSLATE_NOOP("App::Property", "Height to raise during corner action"),
)
obj.Proxy = self
@@ -67,13 +91,17 @@ class ObjectDressup:
return None
def shortcut(self, queue):
'''Determines whether its shorter to twist CW or CCW to align with
the next move'''
"""Determines whether its shorter to twist CW or CCW to align with
the next move"""
# get the vector of the last move
if queue[1].Name in arccommands:
arcLoc = FreeCAD.Vector(queue[2].x + queue[1].I, queue[2].y + queue[1].J, currLocation['Z'])
radvector = arcLoc.sub(queue[1].Placement.Base) # .sub(arcLoc) # vector of chord from center to point
arcLoc = FreeCAD.Vector(
queue[2].x + queue[1].I, queue[2].y + queue[1].J, currLocation["Z"]
)
radvector = arcLoc.sub(
queue[1].Placement.Base
) # .sub(arcLoc) # vector of chord from center to point
# vector of line perp to chord.
v1 = radvector.cross(FreeCAD.Vector(0, 0, 1))
else:
@@ -81,7 +109,9 @@ class ObjectDressup:
# get the vector of the current move
if queue[0].Name in arccommands:
arcLoc = FreeCAD.Vector((queue[1].x + queue[0].I), (queue[1].y + queue[0].J), currLocation['Z'])
arcLoc = FreeCAD.Vector(
(queue[1].x + queue[0].I), (queue[1].y + queue[0].J), currLocation["Z"]
)
radvector = queue[1].Placement.Base.sub(arcLoc) # calculate arcangle
v2 = radvector.cross(FreeCAD.Vector(0, 0, 1))
else:
@@ -93,24 +123,36 @@ class ObjectDressup:
return "CCW"
def segmentAngleXY(self, prevCommand, currCommand, endpos=False, currentZ=0):
'''returns in the starting angle in radians for a Path command.
"""returns in the starting angle in radians for a Path command.
requires the previous command in order to calculate arcs correctly
if endpos = True, return the angle at the end of the segment.'''
if endpos = True, return the angle at the end of the segment."""
if currCommand.Name in arccommands:
arcLoc = FreeCAD.Vector((prevCommand.x + currCommand.I), (prevCommand.y + currCommand.J), currentZ)
arcLoc = FreeCAD.Vector(
(prevCommand.x + currCommand.I),
(prevCommand.y + currCommand.J),
currentZ,
)
if endpos is True:
radvector = arcLoc.sub(currCommand.Placement.Base) # Calculate vector at start of arc
radvector = arcLoc.sub(
currCommand.Placement.Base
) # Calculate vector at start of arc
else:
radvector = arcLoc.sub(prevCommand.Placement.Base) # Calculate vector at end of arc
radvector = arcLoc.sub(
prevCommand.Placement.Base
) # Calculate vector at end of arc
v1 = radvector.cross(FreeCAD.Vector(0, 0, 1))
if currCommand.Name in ["G2", "G02"]:
v1 = D.rotate2D(v1, math.radians(180))
else:
v1 = currCommand.Placement.Base.sub(prevCommand.Placement.Base) # Straight segments are easy
v1 = currCommand.Placement.Base.sub(
prevCommand.Placement.Base
) # Straight segments are easy
myAngle = D.angle(v1, FreeCAD.Base.Vector(1, 0, 0), FreeCAD.Base.Vector(0, 0, -1))
myAngle = D.angle(
v1, FreeCAD.Base.Vector(1, 0, 0), FreeCAD.Base.Vector(0, 0, -1)
)
return myAngle
def getIncidentAngle(self, queue):
@@ -123,18 +165,20 @@ class ObjectDressup:
if angleatstart < 0:
angleatstart = 360 + angleatstart
incident_angle = angleatend-angleatstart
incident_angle = angleatend - angleatstart
return incident_angle
def arcExtension(self, obj, queue):
'''returns gcode for arc extension'''
"""returns gcode for arc extension"""
global currLocation # pylint: disable=global-statement
results = []
offset = obj.offset
# Find the center of the old arc
C = FreeCAD.Base.Vector(queue[2].x + queue[1].I, queue[2].y + queue[1].J, currLocation['Z'])
C = FreeCAD.Base.Vector(
queue[2].x + queue[1].I, queue[2].y + queue[1].J, currLocation["Z"]
)
# Find radius of old arc
R = math.hypot(queue[1].I, queue[1].J)
@@ -165,8 +209,8 @@ class ObjectDressup:
return (results, replace)
def arcTwist(self, obj, queue, lastXY, twistCW=False):
'''returns gcode to do an arc move toward an arc to perform
a corner action twist. Includes lifting and plungeing the knife'''
"""returns gcode to do an arc move toward an arc to perform
a corner action twist. Includes lifting and plungeing the knife"""
global currLocation # pylint: disable=global-statement
pivotheight = obj.pivotheight
@@ -186,7 +230,9 @@ class ObjectDressup:
currLocation.update(retract.Parameters)
# get the center of the destination arc
arccenter = FreeCAD.Base.Vector(queue[1].x + queue[0].I, queue[1].y + queue[0].J, currLocation["Z"])
arccenter = FreeCAD.Base.Vector(
queue[1].x + queue[0].I, queue[1].y + queue[0].J, currLocation["Z"]
)
# The center of the twist arc is the old line end point.
C = queue[1].Placement.Base
@@ -196,7 +242,9 @@ class ObjectDressup:
# find angle of original center to startpoint
v1 = queue[1].Placement.Base.sub(arccenter)
segAngle = D.angle(v1, FreeCAD.Base.Vector(1, 0, 0), FreeCAD.Base.Vector(0, 0, -1))
segAngle = D.angle(
v1, FreeCAD.Base.Vector(1, 0, 0), FreeCAD.Base.Vector(0, 0, -1)
)
# Find angle subtended by the offset
theta = offset / R
@@ -210,14 +258,21 @@ class ObjectDressup:
# calculate endpoints
Bx = arccenter.x + R * math.cos(newangle)
By = arccenter.y + R * math.sin(newangle)
endpointvector = FreeCAD.Base.Vector(Bx, By, currLocation['Z'])
endpointvector = FreeCAD.Base.Vector(Bx, By, currLocation["Z"])
# calculate IJ offsets of twist arc from current position.
offsetvector = C.sub(lastXY)
# add G2/G3 move
arcmove = Path.Command(
arcdir, {"X": endpointvector.x, "Y": endpointvector.y, "I": offsetvector.x, "J": offsetvector.y})
arcdir,
{
"X": endpointvector.x,
"Y": endpointvector.y,
"I": offsetvector.x,
"J": offsetvector.y,
},
)
results.append(arcmove)
currLocation.update(arcmove.Parameters)
@@ -230,11 +285,13 @@ class ObjectDressup:
offsetv = arccenter.sub(endpointvector)
replace = Path.Command(
queue[0].Name, {"X": queue[0].X, "Y": queue[0].Y, "I": offsetv.x, "J": offsetv.y})
queue[0].Name,
{"X": queue[0].X, "Y": queue[0].Y, "I": offsetv.x, "J": offsetv.y},
)
return (results, replace)
def lineExtension(self, obj, queue):
'''returns gcode for line extension'''
"""returns gcode for line extension"""
global currLocation # pylint: disable=global-statement
offset = float(obj.offset)
@@ -243,14 +300,16 @@ class ObjectDressup:
v1 = queue[1].Placement.Base.sub(queue[2].Placement.Base)
# extend the current segment to comp for offset
segAngle = D.angle(v1, FreeCAD.Base.Vector(1, 0, 0), FreeCAD.Base.Vector(0, 0, -1))
segAngle = D.angle(
v1, FreeCAD.Base.Vector(1, 0, 0), FreeCAD.Base.Vector(0, 0, -1)
)
xoffset = math.cos(segAngle) * offset
yoffset = math.sin(segAngle) * offset
newX = currLocation["X"] + xoffset
newY = currLocation["Y"] + yoffset
extendcommand = Path.Command('G1', {"X": newX, "Y": newY})
extendcommand = Path.Command("G1", {"X": newX, "Y": newY})
results.append(extendcommand)
currLocation.update(extendcommand.Parameters)
@@ -259,8 +318,8 @@ class ObjectDressup:
return (results, replace)
def lineTwist(self, obj, queue, lastXY, twistCW=False):
'''returns gcode to do an arc move toward a line to perform
a corner action twist. Includes lifting and plungeing the knife'''
"""returns gcode to do an arc move toward a line to perform
a corner action twist. Includes lifting and plungeing the knife"""
global currLocation # pylint: disable=global-statement
pivotheight = obj.pivotheight
offset = obj.offset
@@ -285,7 +344,9 @@ class ObjectDressup:
v2 = queue[0].Placement.Base.sub(queue[1].Placement.Base)
# calc arc endpoints to twist to
segAngle = D.angle(v2, FreeCAD.Base.Vector(1, 0, 0), FreeCAD.Base.Vector(0, 0, -1))
segAngle = D.angle(
v2, FreeCAD.Base.Vector(1, 0, 0), FreeCAD.Base.Vector(0, 0, -1)
)
xoffset = math.cos(segAngle) * offset
yoffset = math.sin(segAngle) * offset
newX = queue[1].x + xoffset
@@ -297,7 +358,8 @@ class ObjectDressup:
# add the arc move
arcmove = Path.Command(
arcdir, {"X": newX, "Y": newY, "I": I, "J": J}) # add G2/G3 move
arcdir, {"X": newX, "Y": newY, "I": I, "J": J}
) # add G2/G3 move
results.append(arcmove)
currLocation.update(arcmove.Parameters)
@@ -335,11 +397,11 @@ class ObjectDressup:
continue
if curCommand.x is None:
curCommand.x = currLocation['X']
curCommand.x = currLocation["X"]
if curCommand.y is None:
curCommand.y = currLocation['Y']
curCommand.y = currLocation["Y"]
if curCommand.z is None:
curCommand.z = currLocation['Z']
curCommand.z = currLocation["Z"]
# rapid retract triggers exit move, else just add to output
if curCommand.Name in rapidcommands:
@@ -348,7 +410,7 @@ class ObjectDressup:
tempqueue = queue
tempqueue.insert(0, curCommand)
if queue[1].Name in ['G01', 'G1']:
if queue[1].Name in ["G01", "G1"]:
temp = self.lineExtension(obj, tempqueue)
newpath.extend(temp[0])
lastxy = temp[0][-1].Placement.Base
@@ -395,7 +457,7 @@ class ObjectDressup:
#
# DO THE EXTENSION
#
if queue[1].Name in ['G01', 'G1']:
if queue[1].Name in ["G01", "G1"]:
temp = self.lineExtension(obj, queue)
newpath.extend(temp[0])
replace = temp[1]
@@ -410,7 +472,7 @@ class ObjectDressup:
#
# DO THE TWIST
#
if queue[0].Name in ['G01', 'G1']:
if queue[0].Name in ["G01", "G1"]:
temp = self.lineTwist(obj, queue, lastxy, twistCW)
replace = temp[1]
newpath.extend(temp[0])
@@ -433,15 +495,20 @@ class ObjectDressup:
class TaskPanel:
def __init__(self, obj):
self.obj = obj
self.form = FreeCADGui.PySideUic.loadUi(":/panels/DragKnifeEdit.ui")
self.filterAngle = PathGui.QuantitySpinBox(self.form.filterAngle, obj, 'filterAngle')
self.offsetDistance = PathGui.QuantitySpinBox(self.form.offsetDistance, obj, 'offset')
self.pivotHeight = PathGui.QuantitySpinBox(self.form.pivotHeight, obj, 'pivotheight')
self.filterAngle = PathGui.QuantitySpinBox(
self.form.filterAngle, obj, "filterAngle"
)
self.offsetDistance = PathGui.QuantitySpinBox(
self.form.offsetDistance, obj, "offset"
)
self.pivotHeight = PathGui.QuantitySpinBox(
self.form.pivotHeight, obj, "pivotheight"
)
FreeCAD.ActiveDocument.openTransaction(translate("Path_DressupDragKnife", "Edit Dragknife Dress-up"))
FreeCAD.ActiveDocument.openTransaction("Edit Dragknife Dress-up")
def reject(self):
FreeCAD.ActiveDocument.abortTransaction()
@@ -483,7 +550,6 @@ class TaskPanel:
class ViewProviderDressup:
def __init__(self, vobj):
self.Object = vobj.Object
@@ -537,9 +603,16 @@ class CommandDressupDragknife:
# pylint: disable=no-init
def GetResources(self):
return {'Pixmap': 'Path_Dressup',
'MenuText': QtCore.QT_TRANSLATE_NOOP("Path_DressupDragKnife", "DragKnife Dress-up"),
'ToolTip': QtCore.QT_TRANSLATE_NOOP("Path_DressupDragKnife", "Modifies a path to add dragknife corner actions")}
return {
"Pixmap": "Path_Dressup",
"MenuText": QT_TRANSLATE_NOOP(
"Path_DressupDragKnife", "DragKnife Dress-up"
),
"ToolTip": QT_TRANSLATE_NOOP(
"Path_DressupDragKnife",
"Modifies a path to add dragknife corner actions",
),
}
def IsActive(self):
if FreeCAD.ActiveDocument is not None:
@@ -554,33 +627,44 @@ class CommandDressupDragknife:
selection = FreeCADGui.Selection.getSelection()
if len(selection) != 1:
FreeCAD.Console.PrintError(
translate("Path_DressupDragKnife", "Please select one path object")+"\n")
translate("Path_DressupDragKnife", "Please select one path object")
+ "\n"
)
return
if not selection[0].isDerivedFrom("Path::Feature"):
FreeCAD.Console.PrintError(
translate("Path_DressupDragKnife", "The selected object is not a path")+"\n")
translate("Path_DressupDragKnife", "The selected object is not a path")
+ "\n"
)
return
if selection[0].isDerivedFrom("Path::FeatureCompoundPython"):
FreeCAD.Console.PrintError(
translate("Path_DressupDragKnife", "Please select a Path object"))
translate("Path_DressupDragKnife", "Please select a Path object")
)
return
# everything ok!
FreeCAD.ActiveDocument.openTransaction(translate("Path_DressupDragKnife", "Create Dress-up"))
FreeCAD.ActiveDocument.openTransaction("Create Dress-up")
FreeCADGui.addModule("PathScripts.PathDressupDragknife")
FreeCADGui.addModule("PathScripts.PathUtils")
FreeCADGui.doCommand('obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython","DragknifeDressup")')
FreeCADGui.doCommand('PathScripts.PathDressupDragknife.ObjectDressup(obj)')
FreeCADGui.doCommand('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('obj.ViewObject.Proxy = PathScripts.PathDressupDragknife.ViewProviderDressup(obj.ViewObject)')
FreeCADGui.doCommand('Gui.ActiveDocument.getObject(base.Name).Visibility = False')
FreeCADGui.doCommand('obj.filterAngle = 20')
FreeCADGui.doCommand('obj.offset = 2')
FreeCADGui.doCommand('obj.pivotheight = 4')
FreeCADGui.doCommand('obj.ViewObject.Document.setEdit(obj.ViewObject, 0)')
FreeCADGui.doCommand(
'obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython","DragknifeDressup")'
)
FreeCADGui.doCommand("PathScripts.PathDressupDragknife.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(
"obj.ViewObject.Proxy = PathScripts.PathDressupDragknife.ViewProviderDressup(obj.ViewObject)"
)
FreeCADGui.doCommand(
"Gui.ActiveDocument.getObject(base.Name).Visibility = False"
)
FreeCADGui.doCommand("obj.filterAngle = 20")
FreeCADGui.doCommand("obj.offset = 2")
FreeCADGui.doCommand("obj.pivotheight = 4")
FreeCADGui.doCommand("obj.ViewObject.Document.setEdit(obj.ViewObject, 0)")
FreeCAD.ActiveDocument.commitTransaction()
FreeCAD.ActiveDocument.recompute()
@@ -588,6 +672,6 @@ class CommandDressupDragknife:
if FreeCAD.GuiUp:
# register the FreeCAD command
FreeCADGui.addCommand('Path_DressupDragKnife', CommandDressupDragknife())
FreeCADGui.addCommand("Path_DressupDragKnife", CommandDressupDragknife())
FreeCAD.Console.PrintLog("Loading Path_DressupDragKnife... done\n")

View File

@@ -20,29 +20,71 @@
# * *
# ***************************************************************************
''' Used to create CNC machine fixture offsets such as G54,G55, etc...'''
""" Used to create CNC machine fixture offsets such as G54,G55, etc..."""
import FreeCAD
import FreeCADGui
import Path
import PathScripts.PathUtils as PathUtils
from PySide import QtCore#, QtGui
from PySide.QtCore import QT_TRANSLATE_NOOP
# Qt translation handling
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
class Fixture:
def __init__(self,obj):
obj.addProperty("App::PropertyEnumeration", "Fixture", "Path",QtCore.QT_TRANSLATE_NOOP("App::Property","Fixture Offset Number"))
obj.Fixture=['G53','G54','G55','G56','G57','G58','G59','G59.1', 'G59.2', 'G59.3', 'G59.4', 'G59.5','G59.6','G59.7', 'G59.8', 'G59.9']
obj.addProperty("App::PropertyBool","Active","Path",QtCore.QT_TRANSLATE_NOOP("App::Property","Make False, to prevent operation from generating code"))
def __init__(self, obj):
obj.addProperty(
"App::PropertyEnumeration",
"Fixture",
"Path",
QT_TRANSLATE_NOOP("App::Property", "Fixture Offset Number"),
)
obj.Fixture = [
"G53",
"G54",
"G55",
"G56",
"G57",
"G58",
"G59",
"G59.1",
"G59.2",
"G59.3",
"G59.4",
"G59.5",
"G59.6",
"G59.7",
"G59.8",
"G59.9",
]
obj.addProperty(
"App::PropertyBool",
"Active",
"Path",
QT_TRANSLATE_NOOP(
"App::Property", "Make False, to prevent operation from generating code"
),
)
obj.Proxy = self
def execute(self, obj):
fixlist = ['G53', 'G54', 'G55', 'G56', 'G57', 'G58', 'G59', 'G59.1',
'G59.2', 'G59.3', 'G59.4', 'G59.5', 'G59.6', 'G59.7', 'G59.8', 'G59.9']
fixlist = [
"G53",
"G54",
"G55",
"G56",
"G57",
"G58",
"G59",
"G59.1",
"G59.2",
"G59.3",
"G59.4",
"G59.5",
"G59.6",
"G59.7",
"G59.8",
"G59.9",
]
fixture = fixlist.index(obj.Fixture)
obj.Path = Path.Path(str(obj.Fixture))
obj.Label = "Fixture" + str(fixture)
@@ -58,20 +100,19 @@ class Fixture:
class _ViewProviderFixture:
def __init__(self, vobj): # mandatory
# obj.addProperty("App::PropertyFloat","SomePropertyName","PropertyGroup","Description of this property")
vobj.Proxy = self
mode = 2
vobj.setEditorMode('LineWidth', mode)
vobj.setEditorMode('MarkerColor', mode)
vobj.setEditorMode('NormalColor', mode)
vobj.setEditorMode('DisplayMode', mode)
vobj.setEditorMode('BoundingBox', mode)
vobj.setEditorMode('Selectable', mode)
vobj.setEditorMode('ShapeColor', mode)
vobj.setEditorMode('Transparency', mode)
vobj.setEditorMode('Visibility', mode)
vobj.setEditorMode("LineWidth", mode)
vobj.setEditorMode("MarkerColor", mode)
vobj.setEditorMode("NormalColor", mode)
vobj.setEditorMode("DisplayMode", mode)
vobj.setEditorMode("BoundingBox", mode)
vobj.setEditorMode("Selectable", mode)
vobj.setEditorMode("ShapeColor", mode)
vobj.setEditorMode("Transparency", mode)
vobj.setEditorMode("Visibility", mode)
def __getstate__(self): # mandatory
return None
@@ -85,15 +126,15 @@ class _ViewProviderFixture:
def onChanged(self, vobj, prop): # optional
# pylint: disable=unused-argument
mode = 2
vobj.setEditorMode('LineWidth', mode)
vobj.setEditorMode('MarkerColor', mode)
vobj.setEditorMode('NormalColor', mode)
vobj.setEditorMode('DisplayMode', mode)
vobj.setEditorMode('BoundingBox', mode)
vobj.setEditorMode('Selectable', mode)
vobj.setEditorMode('ShapeColor', mode)
vobj.setEditorMode('Transparency', mode)
vobj.setEditorMode('Visibility', mode)
vobj.setEditorMode("LineWidth", mode)
vobj.setEditorMode("MarkerColor", mode)
vobj.setEditorMode("NormalColor", mode)
vobj.setEditorMode("DisplayMode", mode)
vobj.setEditorMode("BoundingBox", mode)
vobj.setEditorMode("Selectable", mode)
vobj.setEditorMode("ShapeColor", mode)
vobj.setEditorMode("Transparency", mode)
vobj.setEditorMode("Visibility", mode)
def updateData(self, vobj, prop): # optional
# this is executed when a property of the APP OBJECT changes
@@ -109,11 +150,14 @@ class _ViewProviderFixture:
class CommandPathFixture:
def GetResources(self):
return {'Pixmap': 'Path_Datums',
'MenuText': QtCore.QT_TRANSLATE_NOOP("Path_Fixture", "Fixture"),
'ToolTip': QtCore.QT_TRANSLATE_NOOP("Path_Fixture", "Creates a Fixture Offset object")}
return {
"Pixmap": "Path_Datums",
"MenuText": QT_TRANSLATE_NOOP("PathFixture", "Fixture"),
"ToolTip": QT_TRANSLATE_NOOP(
"PathFixture", "Creates a Fixture Offset object"
),
}
def IsActive(self):
if FreeCAD.ActiveDocument is not None:
@@ -123,9 +167,9 @@ class CommandPathFixture:
return False
def Activated(self):
FreeCAD.ActiveDocument.openTransaction(translate("Path_Fixture", "Create a Fixture Offset"))
FreeCAD.ActiveDocument.openTransaction("Create a Fixture Offset")
FreeCADGui.addModule("PathScripts.PathFixture")
snippet = '''
snippet = """
import Path
import PathScripts
from PathScripts import PathUtils
@@ -137,14 +181,15 @@ PathScripts.PathFixture._ViewProviderFixture(obj.ViewObject)
PathUtils.addToJob(obj)
'''
"""
FreeCADGui.doCommand(snippet)
FreeCAD.ActiveDocument.commitTransaction()
FreeCAD.ActiveDocument.recompute()
if FreeCAD.GuiUp:
# register the FreeCAD command
FreeCADGui.addCommand('Path_Fixture', CommandPathFixture())
FreeCADGui.addCommand("PathFixture", CommandPathFixture())
FreeCAD.Console.PrintLog("Loading PathFixture... done\n")

View File

@@ -31,7 +31,8 @@ from PySide import QtCore
# lazily loaded modules
from lazy_loader.lazy_loader import LazyLoader
Part = LazyLoader('Part', globals(), 'Part')
Part = LazyLoader("Part", globals(), "Part")
__title__ = "PathGeom - geometry utilities for Path"
__author__ = "sliptonic (Brad Collette)"
@@ -40,16 +41,19 @@ __doc__ = "Functions to extract and convert between Path.Command and Part.Edge a
Tolerance = 0.000001
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
#PathLog.trackModule(PathLog.thisModule())
translate = FreeCAD.Qt.translate
if False:
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
PathLog.trackModule(PathLog.thisModule())
else:
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
# Qt translation handling
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
class Side:
"""Class to determine and define the side a Path is on, or Vectors are in relation to each other."""
Left = +1
Left = +1
Right = -1
Straight = 0
On = 0
@@ -59,10 +63,10 @@ class Side:
"""toString(side)
Returns a string representation of the enum value."""
if side == cls.Left:
return 'Left'
return "Left"
if side == cls.Right:
return 'Right'
return 'On'
return "Right"
return "On"
@classmethod
def of(cls, ptRef, pt):
@@ -71,72 +75,89 @@ class Side:
If both Points are viewed as vectors with their origin in (0,0,0)
then the two vectors either form a straight line (On) or pt
lies in the left or right hemisphere in regards to ptRef."""
d = -ptRef.x*pt.y + ptRef.y*pt.x
d = -ptRef.x * pt.y + ptRef.y * pt.x
if d < 0:
return cls.Left
if d > 0:
return cls.Right
return cls.Straight
CmdMoveRapid = ['G0', 'G00']
CmdMoveStraight = ['G1', 'G01']
CmdMoveCW = ['G2', 'G02']
CmdMoveCCW = ['G3', 'G03']
CmdMoveArc = CmdMoveCW + CmdMoveCCW
CmdMove = CmdMoveStraight + CmdMoveArc
CmdMoveAll = CmdMove + CmdMoveRapid
CmdMoveRapid = ["G0", "G00"]
CmdMoveStraight = ["G1", "G01"]
CmdMoveCW = ["G2", "G02"]
CmdMoveCCW = ["G3", "G03"]
CmdMoveArc = CmdMoveCW + CmdMoveCCW
CmdMove = CmdMoveStraight + CmdMoveArc
CmdMoveAll = CmdMove + CmdMoveRapid
def isRoughly(float1, float2, error=Tolerance):
"""isRoughly(float1, float2, [error=Tolerance])
Returns true if the two values are the same within a given error."""
return math.fabs(float1 - float2) <= error
def pointsCoincide(p1, p2, error=Tolerance):
"""pointsCoincide(p1, p2, [error=Tolerance])
Return True if two points are roughly identical (see also isRoughly)."""
return isRoughly(p1.x, p2.x, error) and isRoughly(p1.y, p2.y, error) and isRoughly(p1.z, p2.z, error)
return (
isRoughly(p1.x, p2.x, error)
and isRoughly(p1.y, p2.y, error)
and isRoughly(p1.z, p2.z, error)
)
def edgesMatch(e0, e1, error=Tolerance):
"""edgesMatch(e0, e1, [error=Tolerance]
Return true if the edges start and end at the same point and have the same type of curve."""
if type(e0.Curve) != type(e1.Curve) or len(e0.Vertexes) != len(e1.Vertexes):
return False
return all(pointsCoincide(e0.Vertexes[i].Point, e1.Vertexes[i].Point, error) for i in range(len(e0.Vertexes)))
return all(
pointsCoincide(e0.Vertexes[i].Point, e1.Vertexes[i].Point, error)
for i in range(len(e0.Vertexes))
)
def edgeConnectsTo(edge, vector, error=Tolerance):
"""edgeConnectsTop(edge, vector, error=Tolerance)
Returns True if edge connects to given vector."""
return pointsCoincide(edge.valueAt(edge.FirstParameter), vector, error) or pointsCoincide(edge.valueAt(edge.LastParameter), vector, error)
return pointsCoincide(
edge.valueAt(edge.FirstParameter), vector, error
) or pointsCoincide(edge.valueAt(edge.LastParameter), vector, error)
def getAngle(vector):
"""getAngle(vector)
Returns the angle [-pi,pi] of a vector using the X-axis as the reference.
Positive angles for vertexes in the upper hemisphere (positive y values)
and negative angles for the lower hemisphere."""
a = vector.getAngle(Vector(1,0,0))
a = vector.getAngle(Vector(1, 0, 0))
if vector.y < 0:
return -a
return a
def diffAngle(a1, a2, direction = 'CW'):
def diffAngle(a1, a2, direction="CW"):
"""diffAngle(a1, a2, [direction='CW'])
Returns the difference between two angles (a1 -> a2) into a given direction."""
if direction == 'CW':
if direction == "CW":
while a1 < a2:
a1 += 2*math.pi
a1 += 2 * math.pi
a = a1 - a2
else:
while a2 < a1:
a2 += 2*math.pi
a2 += 2 * math.pi
a = a2 - a1
return a
def isVertical(obj):
'''isVertical(obj) ... answer True if obj points into Z'''
"""isVertical(obj) ... answer True if obj points into Z"""
if type(obj) == FreeCAD.Vector:
return isRoughly(obj.x, 0) and isRoughly(obj.y, 0)
if obj.ShapeType == 'Face':
if obj.ShapeType == "Face":
if type(obj.Surface) == Part.Plane:
return isHorizontal(obj.Surface.Axis)
if type(obj.Surface) == Part.Cylinder or type(obj.Surface) == Part.Cone:
@@ -148,30 +169,39 @@ def isVertical(obj):
if type(obj.Surface) == Part.SurfaceOfRevolution:
return isHorizontal(obj.Surface.Direction)
if type(obj.Surface) != Part.BSplineSurface:
PathLog.info(translate('PathGeom', "face %s not handled, assuming not vertical") % type(obj.Surface))
PathLog.info(
translate("PathGeom", "face %s not handled, assuming not vertical")
% type(obj.Surface)
)
return None
if obj.ShapeType == 'Edge':
if obj.ShapeType == "Edge":
if type(obj.Curve) == Part.Line or type(obj.Curve) == Part.LineSegment:
return isVertical(obj.Vertexes[1].Point - obj.Vertexes[0].Point)
if type(obj.Curve) == Part.Circle or type(obj.Curve) == Part.Ellipse: # or type(obj.Curve) == Part.BSplineCurve:
if (
type(obj.Curve) == Part.Circle or type(obj.Curve) == Part.Ellipse
): # or type(obj.Curve) == Part.BSplineCurve:
return isHorizontal(obj.Curve.Axis)
if type(obj.Curve) == Part.BezierCurve:
# the current assumption is that a bezier curve is vertical if its end points are vertical
return isVertical(obj.Curve.EndPoint - obj.Curve.StartPoint)
if type(obj.Curve) != Part.BSplineCurve:
PathLog.info(translate('PathGeom', "edge %s not handled, assuming not vertical") % type(obj.Curve))
PathLog.info(
translate("PathGeom", "edge %s not handled, assuming not vertical")
% type(obj.Curve)
)
return None
PathLog.error(translate('PathGeom', "isVertical(%s) not supported") % obj)
PathLog.error(translate("PathGeom", "isVertical(%s) not supported") % obj)
return None
def isHorizontal(obj):
'''isHorizontal(obj) ... answer True if obj points into X or Y'''
"""isHorizontal(obj) ... answer True if obj points into X or Y"""
if type(obj) == FreeCAD.Vector:
return isRoughly(obj.z, 0)
if obj.ShapeType == 'Face':
if obj.ShapeType == "Face":
if type(obj.Surface) == Part.Plane:
return isVertical(obj.Surface.Axis)
if type(obj.Surface) == Part.Cylinder or type(obj.Surface) == Part.Cone:
@@ -184,18 +214,20 @@ def isHorizontal(obj):
return isVertical(obj.Surface.Direction)
return isRoughly(obj.BoundBox.ZLength, 0.0)
if obj.ShapeType == 'Edge':
if obj.ShapeType == "Edge":
if type(obj.Curve) == Part.Line or type(obj.Curve) == Part.LineSegment:
return isHorizontal(obj.Vertexes[1].Point - obj.Vertexes[0].Point)
if type(obj.Curve) == Part.Circle or type(obj.Curve) == Part.Ellipse: # or type(obj.Curve) == Part.BSplineCurve:
if (
type(obj.Curve) == Part.Circle or type(obj.Curve) == Part.Ellipse
): # or type(obj.Curve) == Part.BSplineCurve:
return isVertical(obj.Curve.Axis)
return isRoughly(obj.BoundBox.ZLength, 0.0)
PathLog.error(translate('PathGeom', "isHorizontal(%s) not supported") % obj)
PathLog.error(translate("PathGeom", "isHorizontal(%s) not supported") % obj)
return None
def commandEndPoint(cmd, defaultPoint = Vector(), X='X', Y='Y', Z='Z'):
def commandEndPoint(cmd, defaultPoint=Vector(), X="X", Y="Y", Z="Z"):
"""commandEndPoint(cmd, [defaultPoint=Vector()], [X='X'], [Y='Y'], [Z='Z'])
Extracts the end point from a Path Command."""
x = cmd.Parameters.get(X, defaultPoint.x)
@@ -203,11 +235,13 @@ def commandEndPoint(cmd, defaultPoint = Vector(), X='X', Y='Y', Z='Z'):
z = cmd.Parameters.get(Z, defaultPoint.z)
return Vector(x, y, z)
def xy(point):
"""xy(point)
Convenience function to return the projection of the Vector in the XY-plane."""
return Vector(point.x, point.y, 0)
def speedBetweenPoints(p0, p1, hSpeed, vSpeed):
if isRoughly(hSpeed, vSpeed):
return hSpeed
@@ -223,7 +257,10 @@ def speedBetweenPoints(p0, p1, hSpeed, vSpeed):
pitch = pitch + 1
while pitch > 1:
pitch = pitch - 1
PathLog.debug(" pitch = %g %g (%.2f, %.2f, %.2f) -> %.2f" % (pitch, math.atan2(xy(d).Length, d.z), d.x, d.y, d.z, xy(d).Length))
PathLog.debug(
" pitch = %g %g (%.2f, %.2f, %.2f) -> %.2f"
% (pitch, math.atan2(xy(d).Length, d.z), d.x, d.y, d.z, xy(d).Length)
)
speed = vSpeed + pitch * (hSpeed - vSpeed)
if speed > hSpeed and speed > vSpeed:
return max(hSpeed, vSpeed)
@@ -231,7 +268,8 @@ def speedBetweenPoints(p0, p1, hSpeed, vSpeed):
return min(hSpeed, vSpeed)
return speed
def cmdsForEdge(edge, flip = False, useHelixForBSpline = True, segm = 50, hSpeed = 0, vSpeed = 0):
def cmdsForEdge(edge, flip=False, useHelixForBSpline=True, segm=50, hSpeed=0, vSpeed=0):
"""cmdsForEdge(edge, flip=False, useHelixForBSpline=True, segm=50) -> List(Path.Command)
Returns a list of Path.Command representing the given edge.
If flip is True the edge is considered to be backwards.
@@ -240,31 +278,67 @@ def cmdsForEdge(edge, flip = False, useHelixForBSpline = True, segm = 50, hSpeed
no direct Path.Command mapping and will be approximated by straight segments.
segm is a factor for the segmentation of arbitrary curves not mapped to G1/2/3
commands. The higher the value the more segments will be used."""
pt = edge.valueAt(edge.LastParameter) if not flip else edge.valueAt(edge.FirstParameter)
params = {'X': pt.x, 'Y': pt.y, 'Z': pt.z}
pt = (
edge.valueAt(edge.LastParameter)
if not flip
else edge.valueAt(edge.FirstParameter)
)
params = {"X": pt.x, "Y": pt.y, "Z": pt.z}
if type(edge.Curve) == Part.Line or type(edge.Curve) == Part.LineSegment:
if hSpeed > 0 and vSpeed > 0:
pt2 = edge.valueAt(edge.FirstParameter) if not flip else edge.valueAt(edge.LastParameter)
params.update({'F': speedBetweenPoints(pt, pt2, hSpeed, vSpeed)})
commands = [Path.Command('G1', params)]
pt2 = (
edge.valueAt(edge.FirstParameter)
if not flip
else edge.valueAt(edge.LastParameter)
)
params.update({"F": speedBetweenPoints(pt, pt2, hSpeed, vSpeed)})
commands = [Path.Command("G1", params)]
else:
p1 = edge.valueAt(edge.FirstParameter) if not flip else edge.valueAt(edge.LastParameter)
p2 = edge.valueAt((edge.FirstParameter + edge.LastParameter)/2)
p1 = (
edge.valueAt(edge.FirstParameter)
if not flip
else edge.valueAt(edge.LastParameter)
)
p2 = edge.valueAt((edge.FirstParameter + edge.LastParameter) / 2)
p3 = pt
if hasattr(edge.Curve, 'Axis') and ((type(edge.Curve) == Part.Circle and isRoughly(edge.Curve.Axis.x, 0) and isRoughly(edge.Curve.Axis.y, 0)) or (useHelixForBSpline and type(edge.Curve) == Part.BSplineCurve)):
if hasattr(edge.Curve, "Axis") and (
(
type(edge.Curve) == Part.Circle
and isRoughly(edge.Curve.Axis.x, 0)
and isRoughly(edge.Curve.Axis.y, 0)
)
or (useHelixForBSpline and type(edge.Curve) == Part.BSplineCurve)
):
# This is an arc or a helix and it should be represented by a simple G2/G3 command
if edge.Curve.Axis.z < 0:
cmd = 'G2' if not flip else 'G3'
cmd = "G2" if not flip else "G3"
else:
cmd = 'G3' if not flip else 'G2'
cmd = "G3" if not flip else "G2"
if pointsCoincide(p1, p3):
# A full circle
offset = edge.Curve.Center - pt
else:
pd = Part.Circle(xy(p1), xy(p2), xy(p3)).Center
PathLog.debug("**** %s.%d: (%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f) -> center=(%.2f, %.2f)" % (cmd, flip, p1.x, p1.y, p1.z, p2.x, p2.y, p2.z, p3.x, p3.y, p3.z, pd.x, pd.y))
PathLog.debug(
"**** %s.%d: (%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f) -> center=(%.2f, %.2f)"
% (
cmd,
flip,
p1.x,
p1.y,
p1.z,
p2.x,
p2.y,
p2.z,
p3.x,
p3.y,
p3.z,
pd.x,
pd.y,
)
)
# Have to calculate the center in the XY plane, using pd leads to an error if this is a helix
pa = xy(p1)
@@ -272,15 +346,21 @@ def cmdsForEdge(edge, flip = False, useHelixForBSpline = True, segm = 50, hSpeed
pc = xy(p3)
offset = Part.Circle(pa, pb, pc).Center - pa
PathLog.debug("**** (%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f)" % (pa.x, pa.y, pa.z, pc.x, pc.y, pc.z))
PathLog.debug("**** (%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f)" % (pb.x, pb.y, pb.z, pd.x, pd.y, pd.z))
PathLog.debug(
"**** (%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f)"
% (pa.x, pa.y, pa.z, pc.x, pc.y, pc.z)
)
PathLog.debug(
"**** (%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f)"
% (pb.x, pb.y, pb.z, pd.x, pd.y, pd.z)
)
PathLog.debug("**** (%.2f, %.2f, %.2f)" % (offset.x, offset.y, offset.z))
params.update({'I': offset.x, 'J': offset.y, 'K': (p3.z - p1.z)/2})
params.update({"I": offset.x, "J": offset.y, "K": (p3.z - p1.z) / 2})
# G2/G3 commands are always performed at hSpeed
if hSpeed > 0:
params.update({'F': hSpeed})
commands = [ Path.Command(cmd, params) ]
params.update({"F": hSpeed})
commands = [Path.Command(cmd, params)]
else:
# We're dealing with a helix or a more complex shape and it has to get approximated
@@ -291,18 +371,19 @@ def cmdsForEdge(edge, flip = False, useHelixForBSpline = True, segm = 50, hSpeed
commands = []
if points:
p0 = points[0]
p0 = points[0]
for p in points[1:]:
params = {'X': p.x, 'Y': p.y, 'Z': p.z}
params = {"X": p.x, "Y": p.y, "Z": p.z}
if hSpeed > 0 and vSpeed > 0:
params['F'] = speedBetweenPoints(p0, p, hSpeed, vSpeed)
cmd = Path.Command('G1', params)
params["F"] = speedBetweenPoints(p0, p, hSpeed, vSpeed)
cmd = Path.Command("G1", params)
# print("***** {}".format(cmd))
commands.append(cmd)
p0 = p
#print commands
# print commands
return commands
def edgeForCmd(cmd, startPoint):
"""edgeForCmd(cmd, startPoint).
Returns an Edge representing the given command, assuming a given startPoint."""
@@ -317,29 +398,50 @@ def edgeForCmd(cmd, startPoint):
return Part.Edge(Part.LineSegment(startPoint, endPoint))
if cmd.Name in CmdMoveArc:
center = startPoint + commandEndPoint(cmd, Vector(0,0,0), 'I', 'J', 'K')
center = startPoint + commandEndPoint(cmd, Vector(0, 0, 0), "I", "J", "K")
A = xy(startPoint - center)
B = xy(endPoint - center)
d = -B.x * A.y + B.y * A.x
if isRoughly(d, 0, 0.005):
PathLog.debug("Half circle arc at: (%.2f, %.2f, %.2f)" % (center.x, center.y, center.z))
PathLog.debug(
"Half circle arc at: (%.2f, %.2f, %.2f)"
% (center.x, center.y, center.z)
)
# we're dealing with half a circle here
angle = getAngle(A) + math.pi/2
angle = getAngle(A) + math.pi / 2
if cmd.Name in CmdMoveCW:
angle -= math.pi
else:
C = A + B
angle = getAngle(C)
PathLog.debug("Arc (%8f) at: (%.2f, %.2f, %.2f) -> angle=%f" % (d, center.x, center.y, center.z, angle / math.pi))
PathLog.debug(
"Arc (%8f) at: (%.2f, %.2f, %.2f) -> angle=%f"
% (d, center.x, center.y, center.z, angle / math.pi)
)
R = A.Length
PathLog.debug("arc: p1=(%.2f, %.2f) p2=(%.2f, %.2f) -> center=(%.2f, %.2f)" % (startPoint.x, startPoint.y, endPoint.x, endPoint.y, center.x, center.y))
PathLog.debug("arc: A=(%.2f, %.2f) B=(%.2f, %.2f) -> d=%.2f" % (A.x, A.y, B.x, B.y, d))
PathLog.debug("arc: R=%.2f angle=%.2f" % (R, angle/math.pi))
PathLog.debug(
"arc: p1=(%.2f, %.2f) p2=(%.2f, %.2f) -> center=(%.2f, %.2f)"
% (startPoint.x, startPoint.y, endPoint.x, endPoint.y, center.x, center.y)
)
PathLog.debug(
"arc: A=(%.2f, %.2f) B=(%.2f, %.2f) -> d=%.2f" % (A.x, A.y, B.x, B.y, d)
)
PathLog.debug("arc: R=%.2f angle=%.2f" % (R, angle / math.pi))
if isRoughly(startPoint.z, endPoint.z):
midPoint = center + Vector(math.cos(angle), math.sin(angle), 0) * R
PathLog.debug("arc: (%.2f, %.2f) -> (%.2f, %.2f) -> (%.2f, %.2f)" % (startPoint.x, startPoint.y, midPoint.x, midPoint.y, endPoint.x, endPoint.y))
PathLog.debug(
"arc: (%.2f, %.2f) -> (%.2f, %.2f) -> (%.2f, %.2f)"
% (
startPoint.x,
startPoint.y,
midPoint.x,
midPoint.y,
endPoint.x,
endPoint.y,
)
)
PathLog.debug("StartPoint:{}".format(startPoint))
PathLog.debug("MidPoint:{}".format(midPoint))
PathLog.debug("EndPoint:{}".format(endPoint))
@@ -350,25 +452,26 @@ def edgeForCmd(cmd, startPoint):
return Part.Edge(Part.Arc(startPoint, midPoint, endPoint))
# It's a Helix
#print('angle: A=%.2f B=%.2f' % (getAngle(A)/math.pi, getAngle(B)/math.pi))
# print('angle: A=%.2f B=%.2f' % (getAngle(A)/math.pi, getAngle(B)/math.pi))
if cmd.Name in CmdMoveCW:
cw = True
else:
cw = False
angle = diffAngle(getAngle(A), getAngle(B), 'CW' if cw else 'CCW')
angle = diffAngle(getAngle(A), getAngle(B), "CW" if cw else "CCW")
height = endPoint.z - startPoint.z
pitch = height * math.fabs(2 * math.pi / angle)
if angle > 0:
cw = not cw
#print("Helix: R=%.2f h=%.2f angle=%.2f pitch=%.2f" % (R, height, angle/math.pi, pitch))
# print("Helix: R=%.2f h=%.2f angle=%.2f pitch=%.2f" % (R, height, angle/math.pi, pitch))
helix = Part.makeHelix(pitch, height, R, 0, not cw)
helix.rotate(Vector(), Vector(0,0,1), 180 * getAngle(A) / math.pi)
helix.rotate(Vector(), Vector(0, 0, 1), 180 * getAngle(A) / math.pi)
e = helix.Edges[0]
helix.translate(startPoint - e.valueAt(e.FirstParameter))
return helix.Edges[0]
return None
def wireForPath(path, startPoint = Vector(0, 0, 0)):
def wireForPath(path, startPoint=Vector(0, 0, 0)):
"""wireForPath(path, [startPoint=Vector(0,0,0)])
Returns a wire representing all move commands found in the given path."""
edges = []
@@ -385,7 +488,8 @@ def wireForPath(path, startPoint = Vector(0, 0, 0)):
return (None, rapid)
return (Part.Wire(edges), rapid)
def wiresForPath(path, startPoint = Vector(0, 0, 0)):
def wiresForPath(path, startPoint=Vector(0, 0, 0)):
"""wiresForPath(path, [startPoint=Vector(0,0,0)])
Returns a collection of wires, each representing a continuous cutting Path in path."""
wires = []
@@ -404,36 +508,37 @@ def wiresForPath(path, startPoint = Vector(0, 0, 0)):
wires.append(Part.Wire(edges))
return wires
def arcToHelix(edge, z0, z1):
"""arcToHelix(edge, z0, z1)
Assuming edge is an arc it'll return a helix matching the arc starting at z0 and rising/falling to z1."""
p1 = edge.valueAt(edge.FirstParameter)
# p2 = edge.valueAt(edge.LastParameter)
cmd = cmdsForEdge(edge)[0]
params = cmd.Parameters
params.update({'Z': z1, 'K': (z1 - z0)/2})
params.update({"Z": z1, "K": (z1 - z0) / 2})
command = Path.Command(cmd.Name, params)
#print("- (%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f): %.2f:%.2f" % (edge.Vertexes[0].X, edge.Vertexes[0].Y, edge.Vertexes[0].Z, edge.Vertexes[1].X, edge.Vertexes[1].Y, edge.Vertexes[1].Z, z0, z1))
#print("- %s -> %s" % (cmd, command))
# print("- (%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f): %.2f:%.2f" % (edge.Vertexes[0].X, edge.Vertexes[0].Y, edge.Vertexes[0].Z, edge.Vertexes[1].X, edge.Vertexes[1].Y, edge.Vertexes[1].Z, z0, z1))
# print("- %s -> %s" % (cmd, command))
return edgeForCmd(command, Vector(p1.x, p1.y, z0))
def helixToArc(edge, z = 0):
def helixToArc(edge, z=0):
"""helixToArc(edge, z=0)
Returns the projection of the helix onto the XY-plane with a given offset."""
p1 = edge.valueAt(edge.FirstParameter)
p2 = edge.valueAt((edge.FirstParameter + edge.LastParameter)/2)
p2 = edge.valueAt((edge.FirstParameter + edge.LastParameter) / 2)
p3 = edge.valueAt(edge.LastParameter)
p01 = Vector(p1.x, p1.y, z)
p02 = Vector(p2.x, p2.y, z)
p03 = Vector(p3.x, p3.y, z)
return Part.Edge(Part.Arc(p01, p02, p03))
def splitArcAt(edge, pt):
"""splitArcAt(edge, pt)
Returns a list of 2 edges which together form the original arc split at the given point.
@@ -443,6 +548,7 @@ def splitArcAt(edge, pt):
e1 = Part.Arc(edge.Curve.copy(), p, edge.LastParameter).toShape()
return [e0, e1]
def splitEdgeAt(edge, pt):
"""splitEdgeAt(edge, pt)
Returns a list of 2 edges, forming the original edge split at the given point.
@@ -456,7 +562,10 @@ def splitEdgeAt(edge, pt):
if type(edge.Curve) == Part.Line or type(edge.Curve) == Part.LineSegment:
# it's a line
return [Part.Edge(Part.LineSegment(p1, p2)), Part.Edge(Part.LineSegment(p2, p3))]
return [
Part.Edge(Part.LineSegment(p1, p2)),
Part.Edge(Part.LineSegment(p2, p3)),
]
elif type(edge.Curve) == Part.Circle:
# it's an arc
return splitArcAt(edge, pt)
@@ -466,6 +575,7 @@ def splitEdgeAt(edge, pt):
aes = splitArcAt(arc, Vector(pt.x, pt.y, 0))
return [arcToHelix(aes[0], p1.z, p2.z), arcToHelix(aes[1], p2.z, p3.z)]
def combineConnectedShapes(shapes):
done = False
while not done:
@@ -474,7 +584,13 @@ def combineConnectedShapes(shapes):
PathLog.debug("shapes: {}".format(shapes))
for shape in shapes:
connected = [f for f in combined if isRoughly(shape.distToShape(f)[0], 0.0)]
PathLog.debug(" {}: connected: {} dist: {}".format(len(combined), connected, [shape.distToShape(f)[0] for f in combined]))
PathLog.debug(
" {}: connected: {} dist: {}".format(
len(combined),
connected,
[shape.distToShape(f)[0] for f in combined],
)
)
if connected:
combined = [f for f in combined if f not in connected]
connected.append(shape)
@@ -485,6 +601,7 @@ def combineConnectedShapes(shapes):
shapes = combined
return shapes
def removeDuplicateEdges(wire):
unique = []
for e in wire.Edges:
@@ -492,22 +609,36 @@ def removeDuplicateEdges(wire):
unique.append(e)
return Part.Wire(unique)
OddsAndEnds = []
def flipEdge(edge):
'''flipEdge(edge)
"""flipEdge(edge)
Flips given edge around so the new Vertexes[0] was the old Vertexes[-1] and vice versa, without changing the shape.
Currently only lines, line segments, circles and arcs are supported.'''
Currently only lines, line segments, circles and arcs are supported."""
if Part.Line == type(edge.Curve) and not edge.Vertexes:
return Part.Edge(Part.Line(edge.valueAt(edge.LastParameter), edge.valueAt(edge.FirstParameter)))
return Part.Edge(
Part.Line(
edge.valueAt(edge.LastParameter), edge.valueAt(edge.FirstParameter)
)
)
elif Part.Line == type(edge.Curve) or Part.LineSegment == type(edge.Curve):
return Part.Edge(Part.LineSegment(edge.Vertexes[-1].Point, edge.Vertexes[0].Point))
return Part.Edge(
Part.LineSegment(edge.Vertexes[-1].Point, edge.Vertexes[0].Point)
)
elif Part.Circle == type(edge.Curve):
# Create an inverted circle
circle = Part.Circle(edge.Curve.Center, -edge.Curve.Axis, edge.Curve.Radius)
# Rotate the circle appropriately so it starts at edge.valueAt(edge.LastParameter)
circle.rotate(FreeCAD.Placement(circle.Center, circle.Axis, 180 - math.degrees(edge.LastParameter + edge.Curve.AngleXU)))
circle.rotate(
FreeCAD.Placement(
circle.Center,
circle.Axis,
180 - math.degrees(edge.LastParameter + edge.Curve.AngleXU),
)
)
# Now the edge always starts at 0 and LastParameter is the value range
arc = Part.Edge(circle, 0, edge.LastParameter - edge.FirstParameter)
return arc
@@ -527,7 +658,7 @@ def flipEdge(edge):
ma = max(knots)
mi = min(knots)
knots = [ma+mi-k for k in knots]
knots = [ma + mi - k for k in knots]
mults.reverse()
weights.reverse()
@@ -535,29 +666,36 @@ def flipEdge(edge):
knots.reverse()
flipped = Part.BSplineCurve()
flipped.buildFromPolesMultsKnots(poles, mults , knots, perio, degree, weights, ratio)
flipped.buildFromPolesMultsKnots(
poles, mults, knots, perio, degree, weights, ratio
)
return Part.Edge(flipped)
elif type(edge.Curve) == Part.OffsetCurve:
return edge.reversed()
global OddsAndEnds # pylint: disable=global-statement
global OddsAndEnds # pylint: disable=global-statement
OddsAndEnds.append(edge)
PathLog.warning(translate('PathGeom', "%s not supported for flipping") % type(edge.Curve))
PathLog.warning(
translate("PathGeom", "%s not supported for flipping") % type(edge.Curve)
)
Wire = []
def flipWire(wire):
'''Flip the entire wire and all its edges so it is being processed the other way around.'''
"""Flip the entire wire and all its edges so it is being processed the other way around."""
Wire.append(wire)
edges = [flipEdge(e) for e in wire.Edges]
edges.reverse()
PathLog.debug(edges)
return Part.Wire(edges)
def makeBoundBoxFace(bBox, offset=0.0, zHeight=0.0):
'''makeBoundBoxFace(bBox, offset=0.0, zHeight=0.0)...
Function to create boundbox face, with possible extra offset and custom Z-height.'''
"""makeBoundBoxFace(bBox, offset=0.0, zHeight=0.0)...
Function to create boundbox face, with possible extra offset and custom Z-height."""
p1 = FreeCAD.Vector(bBox.XMin - offset, bBox.YMin - offset, zHeight)
p2 = FreeCAD.Vector(bBox.XMax + offset, bBox.YMin - offset, zHeight)
p3 = FreeCAD.Vector(bBox.XMax + offset, bBox.YMax + offset, zHeight)
@@ -570,9 +708,10 @@ def makeBoundBoxFace(bBox, offset=0.0, zHeight=0.0):
return Part.Face(Part.Wire([L1, L2, L3, L4]))
# Method to combine faces if connected
def combineHorizontalFaces(faces):
'''combineHorizontalFaces(faces)...
"""combineHorizontalFaces(faces)...
This function successfully identifies and combines multiple connected faces and
works on multiple independent faces with multiple connected faces within the list.
The return value is a list of simplified faces.
@@ -580,7 +719,7 @@ def combineHorizontalFaces(faces):
Attempts to do the same shape connecting failed with TechDraw.findShapeOutline() and
PathGeom.combineConnectedShapes(), so this algorithm was created.
'''
"""
horizontal = list()
offset = 10.0
topFace = None
@@ -594,8 +733,10 @@ def combineHorizontalFaces(faces):
# Make offset compound boundbox solid and cut incoming face extrusions from it
allFaces = Part.makeCompound(faces)
if hasattr(allFaces, "Area") and isRoughly(allFaces.Area, 0.0):
msg = translate('PathGeom',
'Zero working area to process. Check your selection and settings.')
msg = translate(
"PathGeom",
"Zero working area to process. Check your selection and settings.",
)
PathLog.info(msg)
return horizontal
@@ -621,10 +762,12 @@ def combineHorizontalFaces(faces):
for f in cut.Faces:
fbb = f.BoundBox
if isRoughly(fbb.ZMin, 5.0) and isRoughly(fbb.ZMax, 5.0):
if (isRoughly(afbb.XMin - offset, fbb.XMin) and
isRoughly(afbb.XMax + offset, fbb.XMax) and
isRoughly(afbb.YMin - offset, fbb.YMin) and
isRoughly(afbb.YMax + offset, fbb.YMax)):
if (
isRoughly(afbb.XMin - offset, fbb.XMin)
and isRoughly(afbb.XMax + offset, fbb.XMax)
and isRoughly(afbb.YMin - offset, fbb.YMin)
and isRoughly(afbb.YMax + offset, fbb.YMax)
):
topFace = f
else:
innerFaces.append(f)

View File

@@ -23,19 +23,27 @@
import FreeCAD
import FreeCADGui
import Path
from PySide import QtCore
from PySide.QtCore import QT_TRANSLATE_NOOP
__doc__ = """Path Hop object and FreeCAD command"""
# Qt translation handling
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
translate = FreeCAD.Qt.translate
class ObjectHop:
def __init__(self, obj):
obj.addProperty("App::PropertyLink", "NextObject", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property","The object to be reached by this hop"))
obj.addProperty("App::PropertyDistance", "HopHeight", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property","The Z height of the hop"))
obj.addProperty(
"App::PropertyLink",
"NextObject",
"Path",
QT_TRANSLATE_NOOP("App::Property", "The object to be reached by this hop"),
)
obj.addProperty(
"App::PropertyDistance",
"HopHeight",
"Path",
QT_TRANSLATE_NOOP("App::Property", "The Z height of the hop"),
)
obj.Proxy = self
def __getstate__(self):
@@ -69,7 +77,6 @@ class ObjectHop:
class ViewProviderPathHop:
def __init__(self, vobj):
self.Object = vobj.Object
vobj.Proxy = self
@@ -88,11 +95,12 @@ class ViewProviderPathHop:
class CommandPathHop:
def GetResources(self):
return {'Pixmap': 'Path_Hop',
'MenuText': QtCore.QT_TRANSLATE_NOOP("Path_Hop", "Hop"),
'ToolTip': QtCore.QT_TRANSLATE_NOOP("Path_Hop", "Creates a Path Hop object")}
return {
"Pixmap": "Path_Hop",
"MenuText": QT_TRANSLATE_NOOP("Path_Hop", "Hop"),
"ToolTip": QT_TRANSLATE_NOOP("Path_Hop", "Creates a Path Hop object"),
}
def IsActive(self):
if FreeCAD.ActiveDocument is not None:
@@ -107,31 +115,33 @@ class CommandPathHop:
selection = FreeCADGui.Selection.getSelection()
if len(selection) != 1:
FreeCAD.Console.PrintError(
translate("Path_Hop", "Please select one path object")+"\n")
translate("Path_Hop", "Please select one path object") + "\n"
)
return
if not selection[0].isDerivedFrom("Path::Feature"):
FreeCAD.Console.PrintError(
translate("Path_Hop", "The selected object is not a path")+"\n")
translate("Path_Hop", "The selected object is not a path") + "\n"
)
return
FreeCAD.ActiveDocument.openTransaction(
translate("Path_Hop", "Create Hop"))
FreeCAD.ActiveDocument.openTransaction("Create Hop")
FreeCADGui.addModule("PathScripts.PathHop")
FreeCADGui.addModule("PathScripts.PathUtils")
FreeCADGui.doCommand(
'obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython","Hop")')
FreeCADGui.doCommand('PathScripts.PathHop.ObjectHop(obj)')
'obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython","Hop")'
)
FreeCADGui.doCommand("PathScripts.PathHop.ObjectHop(obj)")
FreeCADGui.doCommand("PathScripts.PathHop.ViewProviderPathHop(obj.ViewObject)")
FreeCADGui.doCommand(
'PathScripts.PathHop.ViewProviderPathHop(obj.ViewObject)')
FreeCADGui.doCommand(
'obj.NextObject = FreeCAD.ActiveDocument.' + selection[0].Name)
FreeCADGui.doCommand('PathScripts.PathUtils.addToJob(obj)')
"obj.NextObject = FreeCAD.ActiveDocument." + selection[0].Name
)
FreeCADGui.doCommand("PathScripts.PathUtils.addToJob(obj)")
FreeCAD.ActiveDocument.commitTransaction()
FreeCAD.ActiveDocument.recompute()
if FreeCAD.GuiUp:
# register the FreeCAD command
FreeCADGui.addCommand('Path_Hop', CommandPathHop())
FreeCADGui.addCommand("Path_Hop", CommandPathHop())
FreeCAD.Console.PrintLog("Loading PathHop... done\n")

View File

@@ -20,6 +20,7 @@
# * *
# ***************************************************************************
import FreeCAD
import PathGui
import PathScripts.PathLog as PathLog
import PathScripts.PathUtil as PathUtil
@@ -30,11 +31,17 @@ __author__ = "sliptonic (Brad Collette)"
__url__ = "https://www.freecadweb.org"
__doc__ = "ViewProvider who's main and only task is to assign an icon."
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
#PathLog.trackModule(PathLog.thisModule())
translate = FreeCAD.Qt.translate
if False:
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
PathLog.trackModule(PathLog.thisModule())
else:
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
class ViewProvider(object):
'''Generic view provider to assign an icon.'''
"""Generic view provider to assign an icon."""
def __init__(self, vobj, icon):
self.icon = icon
@@ -50,17 +57,17 @@ class ViewProvider(object):
self.obj = vobj.Object
def __getstate__(self):
attrs = {'icon': self.icon }
if hasattr(self, 'editModule'):
attrs['editModule'] = self.editModule
attrs['editCallback'] = self.editCallback
attrs = {"icon": self.icon}
if hasattr(self, "editModule"):
attrs["editModule"] = self.editModule
attrs["editCallback"] = self.editCallback
return attrs
def __setstate__(self, state):
self.icon = state['icon']
if state.get('editModule', None):
self.editModule = state['editModule']
self.editCallback = state['editCallback']
self.icon = state["icon"]
if state.get("editModule", None):
self.editModule = state["editModule"]
self.editCallback = state["editCallback"]
def getIcon(self):
return ":/icons/Path_{}.svg".format(self.icon)
@@ -70,7 +77,7 @@ class ViewProvider(object):
self.editCallback = callback.__name__
def _onEditCallback(self, edit):
if hasattr(self, 'editModule'):
if hasattr(self, "editModule"):
mod = importlib.import_module(self.editModule)
callback = getattr(mod, self.editCallback)
callback(self.obj, self.vobj, edit)
@@ -88,31 +95,34 @@ class ViewProvider(object):
def setupContextMenu(self, vobj, menu):
# pylint: disable=unused-argument
PathLog.track()
from PySide import QtCore, QtGui
edit = QtCore.QCoreApplication.translate('Path', 'Edit', None)
from PySide import QtGui
edit = translate("Path", "Edit")
action = QtGui.QAction(edit, menu)
action.triggered.connect(self.setEdit)
menu.addAction(action)
_factory = {}
def Attach(vobj, name):
'''Attach(vobj, name) ... attach the appropriate view provider to the view object.
If no view provider was registered for the given name a default IconViewProvider is created.'''
"""Attach(vobj, name) ... attach the appropriate view provider to the view object.
If no view provider was registered for the given name a default IconViewProvider is created."""
PathLog.track(vobj.Object.Label, name)
global _factory # pylint: disable=global-statement
for key,value in PathUtil.keyValueIter(_factory):
global _factory # pylint: disable=global-statement
for key, value in PathUtil.keyValueIter(_factory):
if key == name:
return value(vobj, name)
PathLog.track(vobj.Object.Label, name, 'PathIconViewProvider')
PathLog.track(vobj.Object.Label, name, "PathIconViewProvider")
return ViewProvider(vobj, name)
def RegisterViewProvider(name, provider):
'''RegisterViewProvider(name, provider) ... if an IconViewProvider is created for an object with the given name
an instance of provider is used instead.'''
"""RegisterViewProvider(name, provider) ... if an IconViewProvider is created for an object with the given name
an instance of provider is used instead."""
PathLog.track(name)
global _factory # pylint: disable=global-statement
global _factory # pylint: disable=global-statement
_factory[name] = provider

View File

@@ -20,8 +20,6 @@
# * *
# ***************************************************************************
from PathScripts.PathPostProcessor import PostProcessor
from PySide import QtCore
import FreeCAD
import PathScripts.PathLog as PathLog
import PathScripts.PathPreferences as PathPreferences
@@ -31,6 +29,8 @@ import PathScripts.PathToolController as PathToolController
import PathScripts.PathUtil as PathUtil
import json
import time
from PathScripts.PathPostProcessor import PostProcessor
from PySide import QtCore
# lazily loaded modules
from lazy_loader.lazy_loader import LazyLoader
@@ -75,7 +75,6 @@ def isResourceClone(obj, propLink, resourceName):
def createResourceClone(obj, orig, name, icon):
clone = Draft.clone(orig)
clone.Label = "%s-%s" % (name, orig.Label)
clone.addProperty("App::PropertyString", "PathResource")
@@ -102,9 +101,12 @@ Notification = NotificationClass()
class ObjectJob:
def __init__(self, obj, models, templateFile=None):
self.obj = obj
self.tooltip = None
self.tooltipArgs = None
obj.Proxy = self
obj.addProperty(
"App::PropertyFile",
"PostProcessorOutputFile",
@@ -217,6 +219,15 @@ class ObjectJob:
obj.PostProcessorArgs = PathPreferences.defaultPostProcessorArgs()
obj.GeometryTolerance = PathPreferences.defaultGeometryTolerance()
self.setupOperations(obj)
self.setupSetupSheet(obj)
self.setupBaseModel(obj, models)
self.setupToolTable(obj)
self.setFromTemplateFile(obj, templateFile)
self.setupStock(obj)
def setupOperations(self, obj):
"""setupOperations(obj)... setup the Operations group for the Job object."""
ops = FreeCAD.ActiveDocument.addObject(
"Path::FeatureCompoundPython", "Operations"
)
@@ -228,25 +239,6 @@ class ObjectJob:
obj.setEditorMode("Operations", 2) # hide
obj.setEditorMode("Placement", 2)
self.setupSetupSheet(obj)
self.setupBaseModel(obj, models)
self.setupToolTable(obj)
self.tooltip = None
self.tooltipArgs = None
obj.Proxy = self
self.setFromTemplateFile(obj, templateFile)
if not obj.Stock:
stockTemplate = PathPreferences.defaultStockTemplate()
if stockTemplate:
obj.Stock = PathStock.CreateFromTemplate(obj, json.loads(stockTemplate))
if not obj.Stock:
obj.Stock = PathStock.CreateFromBase(obj)
if obj.Stock.ViewObject:
obj.Stock.ViewObject.Visibility = False
def setupSetupSheet(self, obj):
if not getattr(obj, "SetupSheet", None):
obj.addProperty(
@@ -264,10 +256,13 @@ class ObjectJob:
PathScripts.PathIconViewProvider.Attach(
obj.SetupSheet.ViewObject, "SetupSheet"
)
obj.SetupSheet.Label = "SetupSheet"
self.setupSheet = obj.SetupSheet.Proxy
def setupBaseModel(self, obj, models=None):
PathLog.track(obj.Label, models)
addModels = False
if not hasattr(obj, "Model"):
obj.addProperty(
"App::PropertyLink",
@@ -277,6 +272,11 @@ class ObjectJob:
"PathJob", "The base objects for all operations"
),
)
addModels = True
elif obj.Model is None:
addModels = True
if addModels:
model = FreeCAD.ActiveDocument.addObject(
"App::DocumentObjectGroup", "Model"
)
@@ -287,6 +287,7 @@ class ObjectJob:
[createModelResourceClone(obj, base) for base in models]
)
obj.Model = model
obj.Model.Label = "Model"
if hasattr(obj, "Base"):
PathLog.info(
@@ -297,6 +298,7 @@ class ObjectJob:
obj.removeProperty("Base")
def setupToolTable(self, obj):
addTable = False
if not hasattr(obj, "Tools"):
obj.addProperty(
"App::PropertyLink",
@@ -306,6 +308,11 @@ class ObjectJob:
"PathJob", "Collection of all tool controllers for the job"
),
)
addTable = True
elif obj.Tools is None:
addTable = True
if addTable:
toolTable = FreeCAD.ActiveDocument.addObject(
"App::DocumentObjectGroup", "Tools"
)
@@ -317,6 +324,17 @@ class ObjectJob:
obj.removeProperty("ToolController")
obj.Tools = toolTable
def setupStock(self, obj):
"""setupStock(obj)... setup the Stock for the Job object."""
if not obj.Stock:
stockTemplate = PathPreferences.defaultStockTemplate()
if stockTemplate:
obj.Stock = PathStock.CreateFromTemplate(obj, json.loads(stockTemplate))
if not obj.Stock:
obj.Stock = PathStock.CreateFromBase(obj)
if obj.Stock.ViewObject:
obj.Stock.ViewObject.Visibility = False
def removeBase(self, obj, base, removeFromModel):
if isResourceClone(obj, base, None):
PathUtil.clearExpressionEngine(base)
@@ -403,13 +421,17 @@ class ObjectJob:
obj.Operations.Group = []
obj.Operations = ops
FreeCAD.ActiveDocument.removeObject(name)
ops.Label = label
if label == "Unnamed":
ops.Label = "Operations"
else:
ops.Label = label
def onDocumentRestored(self, obj):
self.setupBaseModel(obj)
self.fixupOperations(obj)
self.setupSetupSheet(obj)
self.setupToolTable(obj)
self.integrityCheck(obj)
obj.setEditorMode("Operations", 2) # hide
obj.setEditorMode("Placement", 2)
@@ -624,7 +646,10 @@ class ObjectJob:
def nextToolNumber(self):
# returns the next available toolnumber in the job
group = self.obj.Tools.Group
return sorted([t.ToolNumber for t in group])[-1] + 1
if len(group) > 0:
return sorted([t.ToolNumber for t in group])[-1] + 1
else:
return 1
def addToolController(self, tc):
group = self.obj.Tools.Group
@@ -680,6 +705,49 @@ class ObjectJob:
for op in self.allOperations():
op.Path.Center = center
def integrityCheck(self, job):
"""integrityCheck(job)... Return True if job has all expected children objects. Attempts to restore any missing children."""
suffix = ""
if len(job.Name) > 3:
suffix = job.Name[3:]
def errorMessage(grp, job):
PathLog.error(
translate("PathJobGui", "{} corrupt in {} job.".format(grp, job.Name))
)
if not job.Operations:
self.setupOperations(job)
job.Operations.Label = "Operations" + suffix
if not job.Operations:
errorMessage("Operations", job)
return False
if not job.SetupSheet:
self.setupSetupSheet(job)
job.SetupSheet.Label = "SetupSheet" + suffix
if not job.SetupSheet:
errorMessage("SetupSheet", job)
return False
if not job.Model:
self.setupBaseModel(job)
job.Model.Label = "Model" + suffix
if not job.Model:
errorMessage("Model", job)
return False
if not job.Stock:
self.setupStock(job)
job.Stock.Label = "Stock" + suffix
if not job.Stock:
errorMessage("Stock", job)
return False
if not job.Tools:
self.setupToolTable(job)
job.Tools.Label = "Tools" + suffix
if not job.Tools:
errorMessage("Tools", job)
return False
return True
@classmethod
def baseCandidates(cls):
"""Answer all objects in the current document which could serve as a Base for a job."""

View File

@@ -167,6 +167,9 @@ class ViewProvider:
def setEdit(self, vobj=None, mode=0):
PathLog.track(mode)
if 0 == mode:
job = self.vobj.Object
if not job.Proxy.integrityCheck(job):
return False
self.openTaskPanel()
return True
@@ -1413,7 +1416,10 @@ class TaskPanel:
def setupUi(self, activate):
self.setupGlobal.setupUi()
self.setupOps.setupUi()
try:
self.setupOps.setupUi()
except Exception as ee:
PathLog.error(str(ee))
self.updateStockEditor(-1, False)
self.setFields()

View File

@@ -20,29 +20,38 @@
# * *
# ***************************************************************************
''' Used for CNC machine plane selection G17,G18,G19 '''
""" Used for CNC machine plane selection G17,G18,G19 """
import FreeCAD
import FreeCADGui
import Path
from PySide import QtCore
# Qt translation handling
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
from PySide.QtCore import QT_TRANSLATE_NOOP
class Plane:
def __init__(self,obj):
obj.addProperty("App::PropertyEnumeration", "SelectionPlane","Path",QtCore.QT_TRANSLATE_NOOP("App::Property","Orientation plane of CNC path"))
obj.SelectionPlane=['XY', 'XZ', 'YZ']
obj.addProperty("App::PropertyBool","Active","Path",QtCore.QT_TRANSLATE_NOOP("App::Property","Make False, to prevent operation from generating code"))
def __init__(self, obj):
obj.addProperty(
"App::PropertyEnumeration",
"SelectionPlane",
"Path",
QT_TRANSLATE_NOOP("App::Property", "Orientation plane of CNC path"),
)
obj.SelectionPlane = ["XY", "XZ", "YZ"]
obj.addProperty(
"App::PropertyBool",
"Active",
"Path",
QT_TRANSLATE_NOOP(
"App::Property", "Make False, to prevent operation from generating code"
),
)
obj.Proxy = self
def execute(self, obj):
clonelist = ['XY', 'XZ', 'YZ']
clonelist = ["XY", "XZ", "YZ"]
cindx = clonelist.index(str(obj.SelectionPlane))
pathlist = ['G17', 'G18', 'G19']
pathlist = ["G17", "G18", "G19"]
labelindx = clonelist.index(obj.SelectionPlane) + 1
obj.Label = "Plane" + str(labelindx)
if obj.Active:
@@ -54,19 +63,18 @@ class Plane:
class _ViewProviderPlane:
def __init__(self, vobj): # mandatory
vobj.Proxy = self
mode = 2
vobj.setEditorMode('LineWidth', mode)
vobj.setEditorMode('MarkerColor', mode)
vobj.setEditorMode('NormalColor', mode)
vobj.setEditorMode('DisplayMode', mode)
vobj.setEditorMode('BoundingBox', mode)
vobj.setEditorMode('Selectable', mode)
vobj.setEditorMode('ShapeColor', mode)
vobj.setEditorMode('Transparency', mode)
vobj.setEditorMode('Visibility', mode)
vobj.setEditorMode("LineWidth", mode)
vobj.setEditorMode("MarkerColor", mode)
vobj.setEditorMode("NormalColor", mode)
vobj.setEditorMode("DisplayMode", mode)
vobj.setEditorMode("BoundingBox", mode)
vobj.setEditorMode("Selectable", mode)
vobj.setEditorMode("ShapeColor", mode)
vobj.setEditorMode("Transparency", mode)
vobj.setEditorMode("Visibility", mode)
def __getstate__(self): # mandatory
return None
@@ -80,15 +88,15 @@ class _ViewProviderPlane:
def onChanged(self, vobj, prop): # optional
# pylint: disable=unused-argument
mode = 2
vobj.setEditorMode('LineWidth', mode)
vobj.setEditorMode('MarkerColor', mode)
vobj.setEditorMode('NormalColor', mode)
vobj.setEditorMode('DisplayMode', mode)
vobj.setEditorMode('BoundingBox', mode)
vobj.setEditorMode('Selectable', mode)
vobj.setEditorMode('ShapeColor', mode)
vobj.setEditorMode('Transparency', mode)
vobj.setEditorMode('Visibility', mode)
vobj.setEditorMode("LineWidth", mode)
vobj.setEditorMode("MarkerColor", mode)
vobj.setEditorMode("NormalColor", mode)
vobj.setEditorMode("DisplayMode", mode)
vobj.setEditorMode("BoundingBox", mode)
vobj.setEditorMode("Selectable", mode)
vobj.setEditorMode("ShapeColor", mode)
vobj.setEditorMode("Transparency", mode)
vobj.setEditorMode("Visibility", mode)
def updateData(self, vobj, prop): # optional
# this is executed when a property of the APP OBJECT changes
@@ -104,11 +112,14 @@ class _ViewProviderPlane:
class CommandPathPlane:
def GetResources(self):
return {'Pixmap': 'Path_Plane',
'MenuText': QtCore.QT_TRANSLATE_NOOP("Path_Plane", "Selection Plane"),
'ToolTip': QtCore.QT_TRANSLATE_NOOP("Path_Plane", "Create a Selection Plane object")}
return {
"Pixmap": "PathPlane",
"MenuText": QT_TRANSLATE_NOOP("PathPlane", "Selection Plane"),
"ToolTip": QT_TRANSLATE_NOOP(
"PathPlane", "Create a Selection Plane object"
),
}
def IsActive(self):
if FreeCAD.ActiveDocument is not None:
@@ -118,10 +129,9 @@ class CommandPathPlane:
return False
def Activated(self):
FreeCAD.ActiveDocument.openTransaction(
translate("Path_Plane", "Create a Selection Plane object"))
FreeCAD.ActiveDocument.openTransaction("Create a Selection Plane object")
FreeCADGui.addModule("PathScripts.PathPlane")
snippet = '''
snippet = """
import Path
import PathScripts
from PathScripts import PathUtils
@@ -132,15 +142,16 @@ obj.Active = True
PathScripts.PathPlane._ViewProviderPlane(obj.ViewObject)
PathUtils.addToJob(obj)
'''
"""
FreeCADGui.doCommand(snippet)
FreeCAD.ActiveDocument.commitTransaction()
FreeCAD.ActiveDocument.recompute()
if FreeCAD.GuiUp:
# register the FreeCAD command
FreeCADGui.addCommand('Path_Plane', CommandPathPlane())
FreeCADGui.addCommand("Path_Plane", CommandPathPlane())
FreeCAD.Console.PrintLog("Loading PathPlane... done\n")

View File

@@ -20,7 +20,7 @@
# * *
# ***************************************************************************
''' Post Process command that will make use of the Output File and Post Processor entries in PathJob '''
""" Post Process command that will make use of the Output File and Post Processor entries in PathJob """
from __future__ import print_function
@@ -37,17 +37,13 @@ import os
from PathScripts.PathPostProcessor import PostProcessor
from PySide import QtCore, QtGui
from datetime import datetime
from PySide.QtCore import QT_TRANSLATE_NOOP
LOG_MODULE = PathLog.thisModule()
PathLog.setLevel(PathLog.Level.INFO, LOG_MODULE)
# Qt translation handling
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
class _TempObject:
# pylint: disable=no-init
Path = None
@@ -57,14 +53,15 @@ class _TempObject:
class DlgSelectPostProcessor:
def __init__(self, parent=None):
# pylint: disable=unused-argument
self.dialog = FreeCADGui.PySideUic.loadUi(":/panels/DlgSelectPostProcessor.ui")
firstItem = None
for post in PathPreferences.allEnabledPostProcessors():
item = QtGui.QListWidgetItem(post)
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
item.setFlags(
QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled
)
self.dialog.lwPostProcessor.addItem(item)
if not firstItem:
firstItem = item
@@ -103,50 +100,52 @@ class CommandPathPost:
path = job.PostProcessorOutputFile
filename = path
if '%D' in filename:
if "%D" in filename:
D = FreeCAD.ActiveDocument.FileName
if D:
D = os.path.dirname(D)
# in case the document is in the current working directory
if not D:
D = '.'
D = "."
else:
FreeCAD.Console.PrintError("Please save document in order to resolve output path!\n")
FreeCAD.Console.PrintError(
"Please save document in order to resolve output path!\n"
)
return None
filename = filename.replace('%D', D)
filename = filename.replace("%D", D)
if '%d' in filename:
if "%d" in filename:
d = FreeCAD.ActiveDocument.Label
filename = filename.replace('%d', d)
filename = filename.replace("%d", d)
if '%j' in filename:
if "%j" in filename:
j = job.Label
filename = filename.replace('%j', j)
filename = filename.replace("%j", j)
if '%M' in filename:
if "%M" in filename:
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Macro")
M = pref.GetString("MacroPath", FreeCAD.getUserAppDataDir())
filename = filename.replace('%M', M)
filename = filename.replace("%M", M)
if '%s' in filename:
if "%s" in filename:
if job.SplitOutput:
filename = filename.replace('%s', '_'+str(self.subpart))
filename = filename.replace("%s", "_" + str(self.subpart))
self.subpart += 1
else:
filename = filename.replace('%s', '')
filename = filename.replace("%s", "")
policy = PathPreferences.defaultOutputPolicy()
openDialog = policy == 'Open File Dialog'
openDialog = policy == "Open File Dialog"
if os.path.isdir(filename) or not os.path.isdir(os.path.dirname(filename)):
# Either the entire filename resolves into a directory or the parent directory doesn't exist.
# Either way I don't know what to do - ask for help
openDialog = True
if os.path.isfile(filename) and not openDialog:
if policy == 'Open File Dialog on conflict':
if policy == "Open File Dialog on conflict":
openDialog = True
elif policy == 'Append Unique ID on conflict':
elif policy == "Append Unique ID on conflict":
fn, ext = os.path.splitext(filename)
nr = fn[-3:]
n = 1
@@ -157,7 +156,9 @@ class CommandPathPost:
filename = "%s%03d%s" % (fn, n, ext)
if openDialog:
foo = QtGui.QFileDialog.getSaveFileName(QtGui.QApplication.activeWindow(), "Output File", filename)
foo = QtGui.QFileDialog.getSaveFileName(
QtGui.QApplication.activeWindow(), "Output File", filename
)
if foo[0]:
filename = foo[0]
else:
@@ -176,10 +177,12 @@ class CommandPathPost:
return dlg.exec_()
def GetResources(self):
return {'Pixmap': 'Path_Post',
'MenuText': QtCore.QT_TRANSLATE_NOOP("Path_Post", "Post Process"),
'Accel': "P, P",
'ToolTip': QtCore.QT_TRANSLATE_NOOP("Path_Post", "Post Process the selected Job")}
return {
"Pixmap": "Path_Post",
"MenuText": QT_TRANSLATE_NOOP("Path_Post", "Post Process"),
"Accel": "P, P",
"ToolTip": QT_TRANSLATE_NOOP("Path_Post", "Post Process the selected Job"),
}
def IsActive(self):
if FreeCAD.ActiveDocument is not None:
@@ -198,10 +201,10 @@ class CommandPathPost:
if hasattr(job, "PostProcessorArgs") and job.PostProcessorArgs:
postArgs = job.PostProcessorArgs
elif hasattr(job, "PostProcessor") and job.PostProcessor:
postArgs = ''
postArgs = ""
postname = self.resolvePostProcessor(job)
filename = '-'
filename = "-"
if postname and needFilename:
filename = self.resolveFileName(job)
@@ -211,12 +214,11 @@ class CommandPathPost:
gcode = processor.export(objs, filename, postArgs)
return (False, gcode, filename)
else:
return (True, '', filename)
return (True, "", filename)
def Activated(self):
PathLog.track()
FreeCAD.ActiveDocument.openTransaction(
translate("Path_Post", "Post Process the Selected path(s)"))
FreeCAD.ActiveDocument.openTransaction("Post Process the Selected path(s)")
FreeCADGui.addModule("PathScripts.PathPost")
# Attempt to figure out what the user wants to post-process
@@ -227,7 +229,9 @@ class CommandPathPost:
selected = FreeCADGui.Selection.getSelectionEx()
if len(selected) > 1:
FreeCAD.Console.PrintError("Please select a single job or other path object\n")
FreeCAD.Console.PrintError(
"Please select a single job or other path object\n"
)
return
elif len(selected) == 1:
sel = selected[0].Object
@@ -267,7 +271,7 @@ class CommandPathPost:
postlist = []
if orderby == 'Fixture':
if orderby == "Fixture":
PathLog.debug("Ordering by Fixture")
# Order by fixture means all operations and tool changes will be completed in one
# fixture before moving to the next.
@@ -279,7 +283,13 @@ class CommandPathPost:
c1 = Path.Command(f)
fobj.Path = Path.Path([c1])
if index != 0:
c2 = Path.Command("G0 Z" + str(job.Stock.Shape.BoundBox.ZMax + job.SetupSheet.ClearanceHeightOffset.Value))
c2 = Path.Command(
"G0 Z"
+ str(
job.Stock.Shape.BoundBox.ZMax
+ job.SetupSheet.ClearanceHeightOffset.Value
)
)
fobj.Path.addCommands(c2)
fobj.InList.append(job)
sublist = [fobj]
@@ -287,7 +297,7 @@ class CommandPathPost:
# Now generate the gcode
for obj in job.Operations.Group:
tc = PathUtil.toolControllerForOp(obj)
if tc is not None and PathUtil.opProperty(obj, 'Active'):
if tc is not None and PathUtil.opProperty(obj, "Active"):
if tc.ToolNumber != currTool or split is True:
sublist.append(tc)
PathLog.debug("Appending TC: {}".format(tc.Name))
@@ -295,7 +305,7 @@ class CommandPathPost:
sublist.append(obj)
postlist.append(sublist)
elif orderby == 'Tool':
elif orderby == "Tool":
PathLog.debug("Ordering by Tool")
# Order by tool means tool changes are minimized.
# all operations with the current tool are processed in the current
@@ -307,7 +317,13 @@ class CommandPathPost:
# create an object to serve as the fixture path
fobj = _TempObject()
c1 = Path.Command(f)
c2 = Path.Command("G0 Z" + str(job.Stock.Shape.BoundBox.ZMax + job.SetupSheet.ClearanceHeightOffset.Value))
c2 = Path.Command(
"G0 Z"
+ str(
job.Stock.Shape.BoundBox.ZMax
+ job.SetupSheet.ClearanceHeightOffset.Value
)
)
fobj.Path = Path.Path([c1, c2])
fobj.InList.append(job)
fixturelist.append(fobj)
@@ -319,16 +335,20 @@ class CommandPathPost:
for idx, obj in enumerate(job.Operations.Group):
# check if the operation is active
active = PathUtil.opProperty(obj, 'Active')
active = PathUtil.opProperty(obj, "Active")
tc = PathUtil.toolControllerForOp(obj)
if tc is None or tc.ToolNumber == currTool and active:
curlist.append(obj)
elif tc.ToolNumber != currTool and currTool is None and active: # first TC
elif (
tc.ToolNumber != currTool and currTool is None and active
): # first TC
sublist.append(tc)
curlist.append(obj)
currTool = tc.ToolNumber
elif tc.ToolNumber != currTool and currTool is not None and active: # TC
elif (
tc.ToolNumber != currTool and currTool is not None and active
): # TC
for fixture in fixturelist:
sublist.append(fixture)
sublist.extend(curlist)
@@ -343,7 +363,7 @@ class CommandPathPost:
sublist.extend(curlist)
postlist.append(sublist)
elif orderby == 'Operation':
elif orderby == "Operation":
PathLog.debug("Ordering by Operation")
# Order by operation means ops are done in each fixture in
# sequence.
@@ -352,7 +372,7 @@ class CommandPathPost:
# Now generate the gcode
for obj in job.Operations.Group:
if PathUtil.opProperty(obj, 'Active'):
if PathUtil.opProperty(obj, "Active"):
sublist = []
PathLog.debug("obj: {}".format(obj.Name))
for f in wcslist:
@@ -360,7 +380,13 @@ class CommandPathPost:
c1 = Path.Command(f)
fobj.Path = Path.Path([c1])
if not firstFixture:
c2 = Path.Command("G0 Z" + str(job.Stock.Shape.BoundBox.ZMax + job.SetupSheet.ClearanceHeightOffset.Value))
c2 = Path.Command(
"G0 Z"
+ str(
job.Stock.Shape.BoundBox.ZMax
+ job.SetupSheet.ClearanceHeightOffset.Value
)
)
fobj.Path.addCommands(c2)
fobj.InList.append(job)
sublist.append(fobj)
@@ -374,7 +400,7 @@ class CommandPathPost:
postlist.append(sublist)
fail = True
rc = ''
rc = ""
if split:
for slist in postlist:
(fail, rc, filename) = self.exportObjectsWith(slist, job)
@@ -400,6 +426,6 @@ class CommandPathPost:
if FreeCAD.GuiUp:
# register the FreeCAD command
FreeCADGui.addCommand('Path_Post', CommandPathPost())
FreeCADGui.addCommand("Path_Post", CommandPathPost())
FreeCAD.Console.PrintLog("Loading PathPost... done\n")

View File

@@ -31,35 +31,36 @@ __author__ = "sliptonic (Brad Collette)"
__url__ = "https://www.freecadweb.org"
__doc__ = "Task panel editor for Properties"
# Qt translation handling
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
#PathLog.trackModule(PathLog.thisModule())
if False:
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
PathLog.trackModule(PathLog.thisModule())
else:
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
class _PropertyEditor(object):
'''Base class of all property editors - just outlines the TableView delegate interface.'''
"""Base class of all property editors - just outlines the TableView delegate interface."""
def __init__(self, obj, prop):
self.obj = obj
self.obj = obj
self.prop = prop
def widget(self, parent):
'''widget(parent) ... called by the delegate to get a new editor widget.
Must be implemented by subclasses and return the widget.'''
pass # pylint: disable=unnecessary-pass
"""widget(parent) ... called by the delegate to get a new editor widget.
Must be implemented by subclasses and return the widget."""
pass # pylint: disable=unnecessary-pass
def setEditorData(self, widget):
'''setEditorData(widget) ... called by the delegate to initialize the editor.
"""setEditorData(widget) ... called by the delegate to initialize the editor.
The widget is the object returned by widget().
Must be implemented by subclasses.'''
pass # pylint: disable=unnecessary-pass
Must be implemented by subclasses."""
pass # pylint: disable=unnecessary-pass
def setModelData(self, widget):
'''setModelData(widget) ... called by the delegate to store new values.
Must be implemented by subclasses.'''
pass # pylint: disable=unnecessary-pass
"""setModelData(widget) ... called by the delegate to store new values.
Must be implemented by subclasses."""
pass # pylint: disable=unnecessary-pass
def propertyValue(self):
return self.obj.getPropertyByName(self.prop)
@@ -70,8 +71,9 @@ class _PropertyEditor(object):
def displayString(self):
return self.propertyValue()
class _PropertyEditorBool(_PropertyEditor):
'''Editor for boolean values - uses a combo box.'''
"""Editor for boolean values - uses a combo box."""
def widget(self, parent):
return QtGui.QComboBox(parent)
@@ -85,21 +87,22 @@ class _PropertyEditorBool(_PropertyEditor):
def setModelData(self, widget):
self.setProperty(widget.currentText() == str(True))
class _PropertyEditorString(_PropertyEditor):
'''Editor for string values - uses a line edit.'''
"""Editor for string values - uses a line edit."""
def widget(self, parent):
return QtGui.QLineEdit(parent)
def setEditorData(self, widget):
text = '' if self.propertyValue() is None else self.propertyValue()
text = "" if self.propertyValue() is None else self.propertyValue()
widget.setText(text)
def setModelData(self, widget):
self.setProperty(widget.text())
class _PropertyEditorQuantity(_PropertyEditor):
class _PropertyEditorQuantity(_PropertyEditor):
def widget(self, parent):
return QtGui.QLineEdit(parent)
@@ -117,23 +120,26 @@ class _PropertyEditorQuantity(_PropertyEditor):
def displayString(self):
if self.propertyValue() is None:
return ''
return ""
return self.propertyValue().getUserPreferred()[0]
class _PropertyEditorAngle(_PropertyEditorQuantity):
'''Editor for angle values - uses a line edit'''
"""Editor for angle values - uses a line edit"""
def defaultQuantity(self):
return FreeCAD.Units.Quantity(0, FreeCAD.Units.Angle)
class _PropertyEditorLength(_PropertyEditorQuantity):
'''Editor for length values - uses a line edit.'''
"""Editor for length values - uses a line edit."""
def defaultQuantity(self):
return FreeCAD.Units.Quantity(0, FreeCAD.Units.Length)
class _PropertyEditorPercent(_PropertyEditor):
'''Editor for percent values - uses a spin box.'''
"""Editor for percent values - uses a spin box."""
def widget(self, parent):
return QtGui.QSpinBox(parent)
@@ -148,8 +154,9 @@ class _PropertyEditorPercent(_PropertyEditor):
def setModelData(self, widget):
self.setProperty(widget.value())
class _PropertyEditorInteger(_PropertyEditor):
'''Editor for integer values - uses a spin box.'''
"""Editor for integer values - uses a spin box."""
def widget(self, parent):
return QtGui.QSpinBox(parent)
@@ -163,8 +170,9 @@ class _PropertyEditorInteger(_PropertyEditor):
def setModelData(self, widget):
self.setProperty(widget.value())
class _PropertyEditorFloat(_PropertyEditor):
'''Editor for float values - uses a double spin box.'''
"""Editor for float values - uses a double spin box."""
def widget(self, parent):
return QtGui.QDoubleSpinBox(parent)
@@ -178,20 +186,20 @@ class _PropertyEditorFloat(_PropertyEditor):
def setModelData(self, widget):
self.setProperty(widget.value())
class _PropertyEditorFile(_PropertyEditor):
class _PropertyEditorFile(_PropertyEditor):
def widget(self, parent):
return QtGui.QLineEdit(parent)
def setEditorData(self, widget):
text = '' if self.propertyValue() is None else self.propertyValue()
text = "" if self.propertyValue() is None else self.propertyValue()
widget.setText(text)
def setModelData(self, widget):
self.setProperty(widget.text())
class _PropertyEditorEnumeration(_PropertyEditor):
class _PropertyEditorEnumeration(_PropertyEditor):
def widget(self, parent):
return QtGui.QComboBox(parent)
@@ -203,25 +211,28 @@ class _PropertyEditorEnumeration(_PropertyEditor):
def setModelData(self, widget):
self.setProperty(widget.currentText())
_EditorFactory = {
'App::PropertyAngle' : _PropertyEditorAngle,
'App::PropertyBool' : _PropertyEditorBool,
'App::PropertyDistance' : _PropertyEditorLength,
'App::PropertyEnumeration' : _PropertyEditorEnumeration,
#'App::PropertyFile' : _PropertyEditorFile,
'App::PropertyFloat' : _PropertyEditorFloat,
'App::PropertyInteger' : _PropertyEditorInteger,
'App::PropertyLength' : _PropertyEditorLength,
'App::PropertyPercent' : _PropertyEditorPercent,
'App::PropertyString' : _PropertyEditorString,
}
"App::PropertyAngle": _PropertyEditorAngle,
"App::PropertyBool": _PropertyEditorBool,
"App::PropertyDistance": _PropertyEditorLength,
"App::PropertyEnumeration": _PropertyEditorEnumeration,
#'App::PropertyFile' : _PropertyEditorFile,
"App::PropertyFloat": _PropertyEditorFloat,
"App::PropertyInteger": _PropertyEditorInteger,
"App::PropertyLength": _PropertyEditorLength,
"App::PropertyPercent": _PropertyEditorPercent,
"App::PropertyString": _PropertyEditorString,
}
def Types():
'''Return the types of properties supported.'''
"""Return the types of properties supported."""
return [t for t in _EditorFactory]
def Editor(obj, prop):
'''Returns an editor class to be used for the given property.'''
"""Returns an editor class to be used for the given property."""
factory = _EditorFactory[obj.getTypeIdOfProperty(prop)]
if factory:
return factory(obj, prop)

View File

@@ -40,12 +40,10 @@ from collections import Counter
from datetime import datetime
import os
import webbrowser
import subprocess
from PySide.QtCore import QT_TRANSLATE_NOOP
# Qt translation handling
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
translate = FreeCAD.Qt.translate
LOG_MODULE = "PathSanity"
@@ -102,11 +100,11 @@ class CommandPathSanity:
def GetResources(self):
return {
"Pixmap": "Path_Sanity",
"MenuText": QtCore.QT_TRANSLATE_NOOP(
"MenuText": QT_TRANSLATE_NOOP(
"Path_Sanity", "Check the path job for common errors"
),
"Accel": "P, S",
"ToolTip": QtCore.QT_TRANSLATE_NOOP(
"ToolTip": QT_TRANSLATE_NOOP(
"Path_Sanity", "Check the path job for common errors"
),
}
@@ -464,7 +462,7 @@ class CommandPathSanity:
)
try:
result = os.system("asciidoctor {} -o {}".format(reportraw, reporthtml))
result = subprocess.run(["asciidoctor", reportraw, "-o", reporthtml])
if str(result) == "32512":
msg = "asciidoctor not found. html cannot be generated."
QtGui.QMessageBox.information(None, "Path Sanity", msg)

View File

@@ -25,41 +25,52 @@ import PathScripts.PathGeom as PathGeom
import PathScripts.PathLog as PathLog
import PathScripts.PathSetupSheetOpPrototype as PathSetupSheetOpPrototype
import PathScripts.PathUtil as PathUtil
import PySide
from PySide.QtCore import QT_TRANSLATE_NOOP
__title__ = "Setup Sheet for a Job."
__author__ = "sliptonic (Brad Collette)"
__url__ = "https://www.freecadweb.org"
__doc__ = "A container for all default values and job specific configuration values."
_RegisteredOps = {}
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
# PathLog.trackModule(PathLog.thisModule())
_RegisteredOps: dict = {}
def translate(context, text, disambig=None):
return PySide.QtCore.QCoreApplication.translate(context, text, disambig)
if False:
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
PathLog.trackModule(PathLog.thisModule())
else:
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
class Template:
# pylint: disable=no-init
HorizRapid = 'HorizRapid'
VertRapid = 'VertRapid'
CoolantMode = 'CoolantMode'
SafeHeightOffset = 'SafeHeightOffset'
SafeHeightExpression = 'SafeHeightExpression'
ClearanceHeightOffset = 'ClearanceHeightOffset'
ClearanceHeightExpression = 'ClearanceHeightExpression'
StartDepthExpression = 'StartDepthExpression'
FinalDepthExpression = 'FinalDepthExpression'
StepDownExpression = 'StepDownExpression'
Fixtures = 'Fixtures'
OrderOutputBy = 'OrderOutputBy'
SplitOutput = 'SplitOutput'
HorizRapid = "HorizRapid"
VertRapid = "VertRapid"
CoolantMode = "CoolantMode"
SafeHeightOffset = "SafeHeightOffset"
SafeHeightExpression = "SafeHeightExpression"
ClearanceHeightOffset = "ClearanceHeightOffset"
ClearanceHeightExpression = "ClearanceHeightExpression"
StartDepthExpression = "StartDepthExpression"
FinalDepthExpression = "FinalDepthExpression"
StepDownExpression = "StepDownExpression"
Fixtures = "Fixtures"
OrderOutputBy = "OrderOutputBy"
SplitOutput = "SplitOutput"
All = [HorizRapid, VertRapid, CoolantMode, SafeHeightOffset, SafeHeightExpression, ClearanceHeightOffset, ClearanceHeightExpression, StartDepthExpression, FinalDepthExpression, StepDownExpression]
All = [
HorizRapid,
VertRapid,
CoolantMode,
SafeHeightOffset,
SafeHeightExpression,
ClearanceHeightOffset,
ClearanceHeightExpression,
StartDepthExpression,
FinalDepthExpression,
StepDownExpression,
]
def _traverseTemplateAttributes(attrs, codec):
@@ -82,46 +93,131 @@ def _traverseTemplateAttributes(attrs, codec):
class SetupSheet:
'''Property container object used by a Job to hold global reference values. '''
"""Property container object used by a Job to hold global reference values."""
TemplateReference = '${SetupSheet}'
TemplateReference = "${SetupSheet}"
DefaultSafeHeightOffset = '3 mm'
DefaultClearanceHeightOffset = '5 mm'
DefaultSafeHeightExpression = "OpStockZMax+${SetupSheet}.SafeHeightOffset"
DefaultSafeHeightOffset = "3 mm"
DefaultClearanceHeightOffset = "5 mm"
DefaultSafeHeightExpression = "OpStockZMax+${SetupSheet}.SafeHeightOffset"
DefaultClearanceHeightExpression = "OpStockZMax+${SetupSheet}.ClearanceHeightOffset"
DefaultStartDepthExpression = 'OpStartDepth'
DefaultFinalDepthExpression = 'OpFinalDepth'
DefaultStepDownExpression = 'OpToolDiameter'
DefaultStartDepthExpression = "OpStartDepth"
DefaultFinalDepthExpression = "OpFinalDepth"
DefaultStepDownExpression = "OpToolDiameter"
DefaultCoolantModes = ['None', 'Flood', 'Mist']
DefaultCoolantModes = ["None", "Flood", "Mist"]
def __init__(self, obj):
self.obj = obj
obj.addProperty('App::PropertySpeed', 'VertRapid', 'ToolController', translate('PathSetupSheet', 'Default speed for horizontal rapid moves.'))
obj.addProperty('App::PropertySpeed', 'HorizRapid', 'ToolController', translate('PathSetupSheet', 'Default speed for vertical rapid moves.'))
obj.addProperty(
"App::PropertySpeed",
"VertRapid",
"ToolController",
QT_TRANSLATE_NOOP(
"App::Property", "Default speed for horizontal rapid moves."
),
)
obj.addProperty(
"App::PropertySpeed",
"HorizRapid",
"ToolController",
QT_TRANSLATE_NOOP(
"App::Property", "Default speed for vertical rapid moves."
),
)
obj.addProperty(
"App::PropertyStringList",
"CoolantModes",
"CoolantMode",
QT_TRANSLATE_NOOP("App::Property", "Coolant Modes"),
)
obj.addProperty(
"App::PropertyEnumeration",
"CoolantMode",
"CoolantMode",
QT_TRANSLATE_NOOP("App::Property", "Default coolant mode."),
)
obj.addProperty(
"App::PropertyLength",
"SafeHeightOffset",
"OperationHeights",
QT_TRANSLATE_NOOP(
"App::Property",
"The usage of this field depends on SafeHeightExpression - by default its value is added to StartDepth and used for SafeHeight of an operation.",
),
)
obj.addProperty(
"App::PropertyString",
"SafeHeightExpression",
"OperationHeights",
QT_TRANSLATE_NOOP(
"App::Property", "Expression set for the SafeHeight of new operations."
),
)
obj.addProperty(
"App::PropertyLength",
"ClearanceHeightOffset",
"OperationHeights",
QT_TRANSLATE_NOOP(
"App::Property",
"The usage of this field depends on ClearanceHeightExpression - by default is value is added to StartDepth and used for ClearanceHeight of an operation.",
),
)
obj.addProperty(
"App::PropertyString",
"ClearanceHeightExpression",
"OperationHeights",
QT_TRANSLATE_NOOP(
"App::Property",
"Expression set for the ClearanceHeight of new operations.",
),
)
obj.addProperty(
"App::PropertyString",
"StartDepthExpression",
"OperationDepths",
QT_TRANSLATE_NOOP(
"App::Property", "Expression used for StartDepth of new operations."
),
)
obj.addProperty(
"App::PropertyString",
"FinalDepthExpression",
"OperationDepths",
QT_TRANSLATE_NOOP(
"App::Property", "Expression used for FinalDepth of new operations."
),
)
obj.addProperty(
"App::PropertyString",
"StepDownExpression",
"OperationDepths",
QT_TRANSLATE_NOOP(
"App::Property", "Expression used for StepDown of new operations."
),
)
obj.addProperty('App::PropertyStringList', 'CoolantModes', 'CoolantMode', translate('PathSetupSheet', 'Coolant Modes'))
obj.addProperty('App::PropertyEnumeration', 'CoolantMode', 'CoolantMode', translate('PathSetupSheet', 'Default coolant mode.'))
obj.SafeHeightOffset = self.decodeAttributeString(self.DefaultSafeHeightOffset)
obj.ClearanceHeightOffset = self.decodeAttributeString(
self.DefaultClearanceHeightOffset
)
obj.SafeHeightExpression = self.decodeAttributeString(
self.DefaultSafeHeightExpression
)
obj.ClearanceHeightExpression = self.decodeAttributeString(
self.DefaultClearanceHeightExpression
)
obj.addProperty('App::PropertyLength', 'SafeHeightOffset', 'OperationHeights', translate('PathSetupSheet', 'The usage of this field depends on SafeHeightExpression - by default its value is added to StartDepth and used for SafeHeight of an operation.'))
obj.addProperty('App::PropertyString', 'SafeHeightExpression', 'OperationHeights', translate('PathSetupSheet', 'Expression set for the SafeHeight of new operations.'))
obj.addProperty('App::PropertyLength', 'ClearanceHeightOffset', 'OperationHeights', translate('PathSetupSheet', 'The usage of this field depends on ClearanceHeightExpression - by default is value is added to StartDepth and used for ClearanceHeight of an operation.'))
obj.addProperty('App::PropertyString', 'ClearanceHeightExpression', 'OperationHeights', translate('PathSetupSheet', 'Expression set for the ClearanceHeight of new operations.'))
obj.addProperty('App::PropertyString', 'StartDepthExpression', 'OperationDepths', translate('PathSetupSheet', 'Expression used for StartDepth of new operations.'))
obj.addProperty('App::PropertyString', 'FinalDepthExpression', 'OperationDepths', translate('PathSetupSheet', 'Expression used for FinalDepth of new operations.'))
obj.addProperty('App::PropertyString', 'StepDownExpression', 'OperationDepths', translate('PathSetupSheet', 'Expression used for StepDown of new operations.'))
obj.SafeHeightOffset = self.decodeAttributeString(self.DefaultSafeHeightOffset)
obj.ClearanceHeightOffset = self.decodeAttributeString(self.DefaultClearanceHeightOffset)
obj.SafeHeightExpression = self.decodeAttributeString(self.DefaultSafeHeightExpression)
obj.ClearanceHeightExpression = self.decodeAttributeString(self.DefaultClearanceHeightExpression)
obj.StartDepthExpression = self.decodeAttributeString(self.DefaultStartDepthExpression)
obj.FinalDepthExpression = self.decodeAttributeString(self.DefaultFinalDepthExpression)
obj.StepDownExpression = self.decodeAttributeString(self.DefaultStepDownExpression)
obj.StartDepthExpression = self.decodeAttributeString(
self.DefaultStartDepthExpression
)
obj.FinalDepthExpression = self.decodeAttributeString(
self.DefaultFinalDepthExpression
)
obj.StepDownExpression = self.decodeAttributeString(
self.DefaultStepDownExpression
)
obj.CoolantModes = self.DefaultCoolantModes
obj.CoolantMode = self.DefaultCoolantModes
@@ -133,22 +229,34 @@ class SetupSheet:
def __setstate__(self, state):
for obj in FreeCAD.ActiveDocument.Objects:
if hasattr(obj, 'Proxy') and obj.Proxy == self:
if hasattr(obj, "Proxy") and obj.Proxy == self:
self.obj = obj
break
return None
def hasDefaultToolRapids(self):
return PathGeom.isRoughly(self.obj.VertRapid.Value, 0) and PathGeom.isRoughly(self.obj.HorizRapid.Value, 0)
return PathGeom.isRoughly(self.obj.VertRapid.Value, 0) and PathGeom.isRoughly(
self.obj.HorizRapid.Value, 0
)
def hasDefaultOperationHeights(self):
if self.obj.SafeHeightOffset.UserString != FreeCAD.Units.Quantity(self.DefaultSafeHeightOffset).UserString:
if (
self.obj.SafeHeightOffset.UserString
!= FreeCAD.Units.Quantity(self.DefaultSafeHeightOffset).UserString
):
return False
if self.obj.ClearanceHeightOffset.UserString != FreeCAD.Units.Quantity(self.DefaultClearanceHeightOffset).UserString:
if (
self.obj.ClearanceHeightOffset.UserString
!= FreeCAD.Units.Quantity(self.DefaultClearanceHeightOffset).UserString
):
return False
if self.obj.SafeHeightExpression != self.decodeAttributeString(self.DefaultSafeHeightExpression):
if self.obj.SafeHeightExpression != self.decodeAttributeString(
self.DefaultSafeHeightExpression
):
return False
if self.obj.ClearanceHeightExpression != self.decodeAttributeString(self.DefaultClearanceHeightExpression):
if self.obj.ClearanceHeightExpression != self.decodeAttributeString(
self.DefaultClearanceHeightExpression
):
return False
return True
@@ -165,7 +273,7 @@ class SetupSheet:
return self.obj.CoolantMode == "None"
def setFromTemplate(self, attrs):
'''setFromTemplate(attrs) ... sets the default values from the given dictionary.'''
"""setFromTemplate(attrs) ... sets the default values from the given dictionary."""
for name in Template.All:
if attrs.get(name) is not None:
setattr(self.obj, name, attrs[name])
@@ -180,15 +288,22 @@ class SetupSheet:
prop = prototype.getProperty(propName)
propertyName = OpPropertyName(opName, propName)
propertyGroup = OpPropertyGroup(opName)
prop.setupProperty(self.obj, propertyName, propertyGroup, prop.valueFromString(value))
prop.setupProperty(
self.obj,
propertyName,
propertyGroup,
prop.valueFromString(value),
)
def templateAttributes(self,
includeRapids=True,
includeCoolantMode=True,
includeHeights=True,
includeDepths=True,
includeOps=None):
'''templateAttributes(includeRapids, includeHeights, includeDepths) ... answers a dictionary with the default values.'''
def templateAttributes(
self,
includeRapids=True,
includeCoolantMode=True,
includeHeights=True,
includeDepths=True,
includeOps=None,
):
"""templateAttributes(includeRapids, includeHeights, includeDepths) ... answers a dictionary with the default values."""
attrs = {}
if includeRapids:
@@ -199,15 +314,19 @@ class SetupSheet:
attrs[Template.CoolantMode] = self.obj.CoolantMode
if includeHeights:
attrs[Template.SafeHeightOffset] = self.obj.SafeHeightOffset.UserString
attrs[Template.SafeHeightExpression] = self.obj.SafeHeightExpression
attrs[Template.ClearanceHeightOffset] = self.obj.ClearanceHeightOffset.UserString
attrs[Template.ClearanceHeightExpression] = self.obj.ClearanceHeightExpression
attrs[Template.SafeHeightOffset] = self.obj.SafeHeightOffset.UserString
attrs[Template.SafeHeightExpression] = self.obj.SafeHeightExpression
attrs[
Template.ClearanceHeightOffset
] = self.obj.ClearanceHeightOffset.UserString
attrs[
Template.ClearanceHeightExpression
] = self.obj.ClearanceHeightExpression
if includeDepths:
attrs[Template.StartDepthExpression] = self.obj.StartDepthExpression
attrs[Template.FinalDepthExpression] = self.obj.FinalDepthExpression
attrs[Template.StepDownExpression] = self.obj.StepDownExpression
attrs[Template.StepDownExpression] = self.obj.StepDownExpression
if includeOps:
for opName in includeOps:
@@ -216,13 +335,15 @@ class SetupSheet:
for propName in op.properties():
prop = OpPropertyName(opName, propName)
if hasattr(self.obj, prop):
settings[propName] = PathUtil.getPropertyValueString(self.obj, prop)
settings[propName] = PathUtil.getPropertyValueString(
self.obj, prop
)
attrs[opName] = settings
return attrs
def expressionReference(self):
'''expressionReference() ... returns the string to be used in expressions'''
"""expressionReference() ... returns the string to be used in expressions"""
# Using the Name here and not the Label (both would be valid) because the Name 'fails early'.
#
# If there is a Name/Label conflict and an expression is bound to the Name we'll get an error
@@ -244,23 +365,27 @@ class SetupSheet:
return self.obj.Name
def encodeAttributeString(self, attr):
'''encodeAttributeString(attr) ... return the encoded string of a template attribute.'''
return PathUtil.toUnicode(attr.replace(self.expressionReference(), self.TemplateReference))
"""encodeAttributeString(attr) ... return the encoded string of a template attribute."""
return PathUtil.toUnicode(
attr.replace(self.expressionReference(), self.TemplateReference)
)
def decodeAttributeString(self, attr):
'''decodeAttributeString(attr) ... return the decoded string of a template attribute.'''
return PathUtil.toUnicode(attr.replace(self.TemplateReference, self.expressionReference()))
"""decodeAttributeString(attr) ... return the decoded string of a template attribute."""
return PathUtil.toUnicode(
attr.replace(self.TemplateReference, self.expressionReference())
)
def encodeTemplateAttributes(self, attrs):
'''encodeTemplateAttributes(attrs) ... return a dictionary with all values encoded.'''
"""encodeTemplateAttributes(attrs) ... return a dictionary with all values encoded."""
return _traverseTemplateAttributes(attrs, self.encodeAttributeString)
def decodeTemplateAttributes(self, attrs):
'''decodeTemplateAttributes(attrs) ... expand template attributes to reference the receiver where applicable.'''
"""decodeTemplateAttributes(attrs) ... expand template attributes to reference the receiver where applicable."""
return _traverseTemplateAttributes(attrs, self.decodeAttributeString)
def operationsWithSettings(self):
'''operationsWithSettings() ... returns a list of operations which currently have some settings defined.'''
"""operationsWithSettings() ... returns a list of operations which currently have some settings defined."""
ops = []
for name, value in PathUtil.keyValueIter(_RegisteredOps):
for prop in value.registeredPropertyNames(name):
@@ -279,27 +404,35 @@ class SetupSheet:
setattr(obj, prop, getattr(self.obj, propName))
except Exception:
PathLog.info("SetupSheet has no support for {}".format(opName))
# traceback.print_exc()
def onDocumentRestored(self, obj):
if not hasattr(obj, 'CoolantModes'):
obj.addProperty('App::PropertyStringList', 'CoolantModes', 'CoolantMode', translate('PathSetupSheet', 'Coolant Modes'))
if not hasattr(obj, "CoolantModes"):
obj.addProperty(
"App::PropertyStringList",
"CoolantModes",
"CoolantMode",
QT_TRANSLATE_NOOP("App::Property", "Coolant Modes"),
)
obj.CoolantModes = self.DefaultCoolantModes
if not hasattr(obj, 'CoolantMode'):
obj.addProperty('App::PropertyEnumeration', 'CoolantMode', 'CoolantMode', translate('PathSetupSheet', 'Default coolant mode.'))
if not hasattr(obj, "CoolantMode"):
obj.addProperty(
"App::PropertyEnumeration",
"CoolantMode",
"CoolantMode",
QT_TRANSLATE_NOOP("App::Property", "Default coolant mode."),
)
obj.CoolantMode = self.DefaultCoolantModes
def Create(name='SetupSheet'):
obj = FreeCAD.ActiveDocument.addObject('App::FeaturePython', name)
def Create(name="SetupSheet"):
obj = FreeCAD.ActiveDocument.addObject("App::FeaturePython", name)
obj.Proxy = SetupSheet(obj)
return obj
class _RegisteredOp(object):
def __init__(self, factory, properties):
self.factory = factory
self.properties = properties
@@ -319,7 +452,7 @@ def RegisterOperation(name, objFactory, setupProperties):
def OpNamePrefix(name):
return name.replace('Path', '').replace(' ', '').replace('_', '')
return name.replace("Path", "").replace(" ", "").replace("_", "")
def OpPropertyName(opName, propName):

View File

@@ -22,7 +22,7 @@
import FreeCAD
import FreeCADGui
import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts.PathGui as PathGui
import PathScripts.PathIconViewProvider as PathIconViewProvider
import PathScripts.PathLog as PathLog
@@ -38,14 +38,8 @@ __url__ = "https://www.freecadweb.org"
__doc__ = "Task panel editor for a SetupSheet"
# Qt translation handling
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
LOGLEVEL = False
if LOGLEVEL:
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
PathLog.trackModule(PathLog.thisModule())
@@ -54,8 +48,8 @@ else:
class ViewProvider:
'''ViewProvider for a SetupSheet.
It's sole job is to provide an icon and invoke the TaskPanel on edit.'''
"""ViewProvider for a SetupSheet.
It's sole job is to provide an icon and invoke the TaskPanel on edit."""
def __init__(self, vobj, name):
PathLog.track(name)
@@ -82,7 +76,7 @@ class ViewProvider:
def getDisplayMode(self, mode):
# pylint: disable=unused-argument
return 'Default'
return "Default"
def setEdit(self, vobj, mode=0):
# pylint: disable=unused-argument
@@ -107,7 +101,7 @@ class ViewProvider:
class Delegate(QtGui.QStyledItemDelegate):
PropertyRole = QtCore.Qt.UserRole + 1
EditorRole = QtCore.Qt.UserRole + 2
EditorRole = QtCore.Qt.UserRole + 2
def createEditor(self, parent, option, index):
# pylint: disable=unused-argument
@@ -133,7 +127,7 @@ class Delegate(QtGui.QStyledItemDelegate):
class OpTaskPanel:
'''Editor for an operation's property default values.
"""Editor for an operation's property default values.
The implementation is a simplified generic property editor with basically 3 fields
- checkbox - if set a default value for the given property is set
- name - a non-editable string with the property name
@@ -141,7 +135,7 @@ class OpTaskPanel:
The specific editor classes for a given property type are implemented in
PathSetupSheetOpPrototypeGui which also provides a factory function. The properties
are displayed in a table, each field occypying a column and each row representing
a single property.'''
a single property."""
def __init__(self, obj, name, op):
self.name = name
@@ -168,7 +162,7 @@ class OpTaskPanel:
self.delegate = Delegate(self.form)
self.model = QtGui.QStandardItemModel(len(self.props), 3, self.form)
self.model.setHorizontalHeaderLabels(['Set', 'Property', 'Value'])
self.model.setHorizontalHeaderLabels(["Set", "Property", "Value"])
for i, name in enumerate(self.props):
prop = self.prototype.getProperty(name)
@@ -179,10 +173,12 @@ class OpTaskPanel:
self.model.setData(self.model.index(i, 0), isset, QtCore.Qt.EditRole)
self.model.setData(self.model.index(i, 1), name, QtCore.Qt.EditRole)
self.model.setData(self.model.index(i, 2), prop, Delegate.PropertyRole)
self.model.setData(self.model.index(i, 2), prop.displayString(), QtCore.Qt.DisplayRole)
self.model.setData(
self.model.index(i, 2), prop.displayString(), QtCore.Qt.DisplayRole
)
self.model.item(i, 0).setCheckable(True)
self.model.item(i, 0).setText('')
self.model.item(i, 0).setText("")
self.model.item(i, 1).setEditable(False)
self.model.item(i, 1).setToolTip(prop.info)
self.model.item(i, 2).setToolTip(prop.info)
@@ -213,7 +209,9 @@ class OpTaskPanel:
propName = self.propertyName(name)
enabled = self.model.item(i, 0).checkState() == QtCore.Qt.Checked
if enabled and not prop.getValue() is None:
if prop.setupProperty(self.obj, propName, self.propertyGroup(), prop.getValue()):
if prop.setupProperty(
self.obj, propName, self.propertyGroup(), prop.getValue()
):
propertiesCreatedRemoved = True
else:
if hasattr(self.obj, propName):
@@ -223,15 +221,21 @@ class OpTaskPanel:
class OpsDefaultEditor:
'''Class to collect and display default property editors for all registered operations.
"""Class to collect and display default property editors for all registered operations.
If a form is given at creation time it will integrate with that form and provide an interface to switch
between the editors of different operations. If no form is provided the class assumes that the UI is
taken care of somehow else and just serves as an interface to all operation editors.'''
taken care of somehow else and just serves as an interface to all operation editors."""
def __init__(self, obj, form):
self.form = form
self.obj = obj
self.ops = sorted([OpTaskPanel(self.obj, name, op) for name, op in PathUtil.keyValueIter(PathSetupSheet._RegisteredOps)], key=lambda op: op.name)
self.ops = sorted(
[
OpTaskPanel(self.obj, name, op)
for name, op in PathUtil.keyValueIter(PathSetupSheet._RegisteredOps)
],
key=lambda op: op.name,
)
if form:
parent = form.tabOpDefaults
for op in self.ops:
@@ -281,7 +285,7 @@ class OpsDefaultEditor:
class GlobalEditor(object):
'''Editor for the global properties which affect almost every operation.'''
"""Editor for the global properties which affect almost every operation."""
def __init__(self, obj, form):
self.form = form
@@ -306,11 +310,13 @@ class GlobalEditor(object):
if val != value:
PathUtil.setProperty(self.obj, name, value)
updateExpression('StartDepthExpression', self.form.setupStartDepthExpr)
updateExpression('FinalDepthExpression', self.form.setupFinalDepthExpr)
updateExpression('StepDownExpression', self.form.setupStepDownExpr)
updateExpression('ClearanceHeightExpression', self.form.setupClearanceHeightExpr)
updateExpression('SafeHeightExpression', self.form.setupSafeHeightExpr)
updateExpression("StartDepthExpression", self.form.setupStartDepthExpr)
updateExpression("FinalDepthExpression", self.form.setupFinalDepthExpr)
updateExpression("StepDownExpression", self.form.setupStepDownExpr)
updateExpression(
"ClearanceHeightExpression", self.form.setupClearanceHeightExpr
)
updateExpression("SafeHeightExpression", self.form.setupSafeHeightExpr)
self.clearanceHeightOffs.updateProperty()
self.safeHeightOffs.updateProperty()
self.rapidVertical.updateProperty()
@@ -318,7 +324,7 @@ class GlobalEditor(object):
self.obj.CoolantMode = self.form.setupCoolantMode.currentText()
def selectInComboBox(self, name, combo):
'''selectInComboBox(name, combo) ... helper function to select a specific value in a combo box.'''
"""selectInComboBox(name, combo) ... helper function to select a specific value in a combo box."""
index = combo.findText(name, QtCore.Qt.MatchFixedString)
if index >= 0:
combo.blockSignals(True)
@@ -349,16 +355,24 @@ class GlobalEditor(object):
self.updateUI()
def setupUi(self):
self.clearanceHeightOffs = PathGui.QuantitySpinBox(self.form.setupClearanceHeightOffs, self.obj, 'ClearanceHeightOffset')
self.safeHeightOffs = PathGui.QuantitySpinBox(self.form.setupSafeHeightOffs, self.obj, 'SafeHeightOffset')
self.rapidHorizontal = PathGui.QuantitySpinBox(self.form.setupRapidHorizontal, self.obj, 'HorizRapid')
self.rapidVertical = PathGui.QuantitySpinBox(self.form.setupRapidVertical, self.obj, 'VertRapid')
self.clearanceHeightOffs = PathGui.QuantitySpinBox(
self.form.setupClearanceHeightOffs, self.obj, "ClearanceHeightOffset"
)
self.safeHeightOffs = PathGui.QuantitySpinBox(
self.form.setupSafeHeightOffs, self.obj, "SafeHeightOffset"
)
self.rapidHorizontal = PathGui.QuantitySpinBox(
self.form.setupRapidHorizontal, self.obj, "HorizRapid"
)
self.rapidVertical = PathGui.QuantitySpinBox(
self.form.setupRapidVertical, self.obj, "VertRapid"
)
self.form.setupCoolantMode.addItems(self.obj.CoolantModes)
self.setFields()
class TaskPanel:
'''TaskPanel for the SetupSheet - if it is being edited directly.'''
"""TaskPanel for the SetupSheet - if it is being edited directly."""
def __init__(self, vobj):
self.vobj = vobj
@@ -368,7 +382,7 @@ class TaskPanel:
self.globalEditor = GlobalEditor(self.obj, self.globalForm)
self.opsEditor = OpsDefaultEditor(self.obj, None)
self.form = [op.form for op in self.opsEditor.ops] + [self.globalForm]
FreeCAD.ActiveDocument.openTransaction(translate("Path_SetupSheet", "Edit SetupSheet"))
FreeCAD.ActiveDocument.openTransaction("Edit SetupSheet")
def reject(self):
self.globalEditor.reject()
@@ -408,12 +422,12 @@ class TaskPanel:
self.opsEditor.setupUi()
def Create(name='SetupSheet'):
'''Create(name='SetupSheet') ... creates a new setup sheet'''
FreeCAD.ActiveDocument.openTransaction(translate("Path_Job", "Create Job"))
def Create(name="SetupSheet"):
"""Create(name='SetupSheet') ... creates a new setup sheet"""
FreeCAD.ActiveDocument.openTransaction("Create Job")
ssheet = PathSetupSheet.Create(name)
PathIconViewProvider.Attach(ssheet, name)
return ssheet
PathIconViewProvider.RegisterViewProvider('SetupSheet', ViewProvider)
PathIconViewProvider.RegisterViewProvider("SetupSheet", ViewProvider)

View File

@@ -31,9 +31,6 @@ __author__ = "sliptonic (Brad Collette)"
__url__ = "https://www.freecadweb.org"
__doc__ = "Task panel editor for a SetupSheet"
# Qt translation handling
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
LOGLEVEL = False
@@ -43,27 +40,32 @@ if LOGLEVEL:
else:
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
class _PropertyEditor(object):
'''Base class of all property editors - just outlines the TableView delegate interface.'''
"""Base class of all property editors - just outlines the TableView delegate interface."""
def __init__(self, prop):
self.prop = prop
def widget(self, parent):
'''widget(parent) ... called by the delegate to get a new editor widget.
Must be implemented by subclasses and return the widget.'''
pass # pylint: disable=unnecessary-pass
"""widget(parent) ... called by the delegate to get a new editor widget.
Must be implemented by subclasses and return the widget."""
pass # pylint: disable=unnecessary-pass
def setEditorData(self, widget):
'''setEditorData(widget) ... called by the delegate to initialize the editor.
"""setEditorData(widget) ... called by the delegate to initialize the editor.
The widget is the object returned by widget().
Must be implemented by subclasses.'''
pass # pylint: disable=unnecessary-pass
Must be implemented by subclasses."""
pass # pylint: disable=unnecessary-pass
def setModelData(self, widget):
'''setModelData(widget) ... called by the delegate to store new values.
Must be implemented by subclasses.'''
pass # pylint: disable=unnecessary-pass
"""setModelData(widget) ... called by the delegate to store new values.
Must be implemented by subclasses."""
pass # pylint: disable=unnecessary-pass
class _PropertyEnumEditor(_PropertyEditor):
'''Editor for enumeration values - uses a combo box.'''
"""Editor for enumeration values - uses a combo box."""
def widget(self, parent):
PathLog.track(self.prop.name, self.prop.getEnumValues())
@@ -82,36 +84,38 @@ class _PropertyEnumEditor(_PropertyEditor):
class _PropertyBoolEditor(_PropertyEditor):
'''Editor for boolean values - uses a combo box.'''
"""Editor for boolean values - uses a combo box."""
def widget(self, parent):
return QtGui.QComboBox(parent)
def setEditorData(self, widget):
widget.clear()
widget.addItems(['false', 'true'])
widget.addItems(["false", "true"])
if not self.prop.getValue() is None:
index = 1 if self.prop.getValue() else 0
widget.setCurrentIndex(index)
def setModelData(self, widget):
self.prop.setValue(widget.currentText() == 'true')
self.prop.setValue(widget.currentText() == "true")
class _PropertyStringEditor(_PropertyEditor):
'''Editor for string values - uses a line edit.'''
"""Editor for string values - uses a line edit."""
def widget(self, parent):
return QtGui.QLineEdit(parent)
def setEditorData(self, widget):
text = '' if self.prop.getValue() is None else self.prop.getValue()
text = "" if self.prop.getValue() is None else self.prop.getValue()
widget.setText(text)
def setModelData(self, widget):
self.prop.setValue(widget.text())
class _PropertyAngleEditor(_PropertyEditor):
'''Editor for angle values - uses a line edit'''
"""Editor for angle values - uses a line edit"""
def widget(self, parent):
return QtGui.QLineEdit(parent)
@@ -125,8 +129,9 @@ class _PropertyAngleEditor(_PropertyEditor):
def setModelData(self, widget):
self.prop.setValue(FreeCAD.Units.Quantity(widget.text()))
class _PropertyLengthEditor(_PropertyEditor):
'''Editor for length values - uses a line edit.'''
"""Editor for length values - uses a line edit."""
def widget(self, parent):
return QtGui.QLineEdit(parent)
@@ -140,8 +145,9 @@ class _PropertyLengthEditor(_PropertyEditor):
def setModelData(self, widget):
self.prop.setValue(FreeCAD.Units.Quantity(widget.text()))
class _PropertyPercentEditor(_PropertyEditor):
'''Editor for percent values - uses a spin box.'''
"""Editor for percent values - uses a spin box."""
def widget(self, parent):
return QtGui.QSpinBox(parent)
@@ -156,8 +162,9 @@ class _PropertyPercentEditor(_PropertyEditor):
def setModelData(self, widget):
self.prop.setValue(widget.value())
class _PropertyIntegerEditor(_PropertyEditor):
'''Editor for integer values - uses a spin box.'''
"""Editor for integer values - uses a spin box."""
def widget(self, parent):
return QtGui.QSpinBox(parent)
@@ -171,8 +178,9 @@ class _PropertyIntegerEditor(_PropertyEditor):
def setModelData(self, widget):
self.prop.setValue(widget.value())
class _PropertyFloatEditor(_PropertyEditor):
'''Editor for float values - uses a double spin box.'''
"""Editor for float values - uses a double spin box."""
def widget(self, parent):
return QtGui.QDoubleSpinBox(parent)
@@ -186,33 +194,35 @@ class _PropertyFloatEditor(_PropertyEditor):
def setModelData(self, widget):
self.prop.setValue(widget.value())
class _PropertyFileEditor(_PropertyEditor):
class _PropertyFileEditor(_PropertyEditor):
def widget(self, parent):
return QtGui.QLineEdit(parent)
def setEditorData(self, widget):
text = '' if self.prop.getValue() is None else self.prop.getValue()
text = "" if self.prop.getValue() is None else self.prop.getValue()
widget.setText(text)
def setModelData(self, widget):
self.prop.setValue(widget.text())
_EditorFactory = {
PathSetupSheetOpPrototype.Property: None,
PathSetupSheetOpPrototype.PropertyAngle: _PropertyAngleEditor,
PathSetupSheetOpPrototype.PropertyBool: _PropertyBoolEditor,
PathSetupSheetOpPrototype.PropertyDistance: _PropertyLengthEditor,
PathSetupSheetOpPrototype.PropertyEnumeration: _PropertyEnumEditor,
PathSetupSheetOpPrototype.PropertyFloat: _PropertyFloatEditor,
PathSetupSheetOpPrototype.PropertyInteger: _PropertyIntegerEditor,
PathSetupSheetOpPrototype.PropertyLength: _PropertyLengthEditor,
PathSetupSheetOpPrototype.PropertyPercent: _PropertyPercentEditor,
PathSetupSheetOpPrototype.PropertyString: _PropertyStringEditor,
}
PathSetupSheetOpPrototype.Property: None,
PathSetupSheetOpPrototype.PropertyAngle: _PropertyAngleEditor,
PathSetupSheetOpPrototype.PropertyBool: _PropertyBoolEditor,
PathSetupSheetOpPrototype.PropertyDistance: _PropertyLengthEditor,
PathSetupSheetOpPrototype.PropertyEnumeration: _PropertyEnumEditor,
PathSetupSheetOpPrototype.PropertyFloat: _PropertyFloatEditor,
PathSetupSheetOpPrototype.PropertyInteger: _PropertyIntegerEditor,
PathSetupSheetOpPrototype.PropertyLength: _PropertyLengthEditor,
PathSetupSheetOpPrototype.PropertyPercent: _PropertyPercentEditor,
PathSetupSheetOpPrototype.PropertyString: _PropertyStringEditor,
}
def Editor(prop):
'''Returns an editor class to be used for the given property.'''
"""Returns an editor class to be used for the given property."""
factory = _EditorFactory[prop.__class__]
if factory:
return factory(prop)

View File

@@ -23,22 +23,22 @@
import FreeCAD
import FreeCADGui
import PathScripts
from PySide import QtCore
from PySide.QtCore import QT_TRANSLATE_NOOP
__doc__ = """Path SimpleCopy command"""
# Qt translation handling
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
translate = FreeCAD.Qt.translate
class CommandPathSimpleCopy:
def GetResources(self):
return {'Pixmap': 'Path_SimpleCopy',
'MenuText': QtCore.QT_TRANSLATE_NOOP("Path_SimpleCopy", "Simple Copy"),
'ToolTip': QtCore.QT_TRANSLATE_NOOP("Path_SimpleCopy", "Creates a non-parametric copy of another path")}
return {
"Pixmap": "Path_SimpleCopy",
"MenuText": QT_TRANSLATE_NOOP("Path_SimpleCopy", "Simple Copy"),
"ToolTip": QT_TRANSLATE_NOOP(
"Path_SimpleCopy", "Creates a non-parametric copy of another path"
),
}
def IsActive(self):
if bool(FreeCADGui.Selection.getSelection()) is False:
@@ -46,7 +46,7 @@ class CommandPathSimpleCopy:
try:
obj = FreeCADGui.Selection.getSelectionEx()[0].Object
return isinstance(obj.Proxy, PathScripts.PathOp.ObjectOp)
except Exception: # pylint: disable=broad-except
except Exception: # pylint: disable=broad-except
return False
def Activated(self):
@@ -54,27 +54,36 @@ class CommandPathSimpleCopy:
selection = FreeCADGui.Selection.getSelection()
if len(selection) != 1:
FreeCAD.Console.PrintError(
translate("Path_SimpleCopy", "Please select exactly one path object")+"\n")
translate("Path_SimpleCopy", "Please select exactly one path object")
+ "\n"
)
return
if not(selection[0].isDerivedFrom("Path::Feature")):
if not (selection[0].isDerivedFrom("Path::Feature")):
FreeCAD.Console.PrintError(
translate("Path_SimpleCopy", "Please select exactly one path object")+"\n")
translate("Path_SimpleCopy", "Please select exactly one path object")
+ "\n"
)
return
FreeCAD.ActiveDocument.openTransaction(
translate("Path_SimpleCopy", "Simple Copy"))
FreeCADGui.doCommand("srcpath = FreeCADGui.Selection.getSelectionEx()[0].Object.Path\n")
FreeCAD.ActiveDocument.openTransaction("Simple Copy")
FreeCADGui.doCommand(
"srcpath = FreeCADGui.Selection.getSelectionEx()[0].Object.Path\n"
)
FreeCADGui.addModule("PathScripts.PathUtils")
FreeCADGui.addModule("PathScripts.PathCustom")
FreeCADGui.doCommand('obj = PathScripts.PathCustom.Create("' + selection[0].Name + '_SimpleCopy")')
FreeCADGui.doCommand('obj.ViewObject.Proxy = 0')
FreeCADGui.doCommand('obj.Gcode = [c.toGCode() for c in srcpath.Commands]')
FreeCADGui.doCommand('PathScripts.PathUtils.addToJob(obj)')
FreeCADGui.doCommand(
'obj = PathScripts.PathCustom.Create("'
+ selection[0].Name
+ '_SimpleCopy")'
)
FreeCADGui.doCommand("obj.ViewObject.Proxy = 0")
FreeCADGui.doCommand("obj.Gcode = [c.toGCode() for c in srcpath.Commands]")
FreeCADGui.doCommand("PathScripts.PathUtils.addToJob(obj)")
FreeCAD.ActiveDocument.commitTransaction()
FreeCAD.ActiveDocument.recompute()
if FreeCAD.GuiUp:
# register the FreeCAD command
FreeCADGui.addCommand('Path_SimpleCopy', CommandPathSimpleCopy())
FreeCADGui.addCommand("Path_SimpleCopy", CommandPathSimpleCopy())

View File

@@ -22,7 +22,7 @@
import FreeCAD
import Path
import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts.PathDressup as PathDressup
import PathScripts.PathGeom as PathGeom
import PathScripts.PathLog as PathLog
@@ -31,13 +31,15 @@ import PathScripts.PathJob as PathJob
import PathSimulator
import math
import os
from PySide.QtCore import QT_TRANSLATE_NOOP
from FreeCAD import Vector, Base
# lazily loaded modules
from lazy_loader.lazy_loader import LazyLoader
Mesh = LazyLoader('Mesh', globals(), 'Mesh')
Part = LazyLoader('Part', globals(), 'Part')
Mesh = LazyLoader("Mesh", globals(), "Mesh")
Part = LazyLoader("Part", globals(), "Part")
if FreeCAD.GuiUp:
import FreeCADGui
@@ -83,7 +85,9 @@ class PathSimulation:
def UpdateProgress(self):
if self.numCommands > 0:
self.taskForm.form.progressBar.setValue(self.iprogress * 100 / self.numCommands)
self.taskForm.form.progressBar.setValue(
self.iprogress * 100 / self.numCommands
)
def Activate(self):
self.initdone = False
@@ -112,7 +116,7 @@ class PathSimulation:
def _populateJobSelection(self, form):
# Make Job selection combobox
setJobIdx = 0
jobName = ''
jobName = ""
jIdx = 0
# Get list of Job objects in active document
jobList = FreeCAD.ActiveDocument.findObjects("Path::FeaturePython", "Job.*")
@@ -122,10 +126,12 @@ class PathSimulation:
guiSelection = FreeCADGui.Selection.getSelectionEx()
if guiSelection: # Identify job selected by user
sel = guiSelection[0]
if hasattr(sel.Object, "Proxy") and isinstance(sel.Object.Proxy, PathJob.ObjectJob):
if hasattr(sel.Object, "Proxy") and isinstance(
sel.Object.Proxy, PathJob.ObjectJob
):
jobName = sel.Object.Name
FreeCADGui.Selection.clearSelection()
# populate the job selection combobox
form.comboJobs.blockSignals(True)
form.comboJobs.clear()
@@ -136,7 +142,7 @@ class PathSimulation:
if j.Name == jobName or jCnt == 1:
setJobIdx = jIdx
jIdx += 1
# Pre-select GUI-selected job in the combobox
if jobName or jCnt == 1:
form.comboJobs.setCurrentIndex(setJobIdx)
@@ -155,19 +161,22 @@ class PathSimulation:
self.numCommands += len(self.operations[i].Path.Commands)
self.stock = self.job.Stock.Shape
if (self.isVoxel):
if self.isVoxel:
maxlen = self.stock.BoundBox.XLength
if (maxlen < self.stock.BoundBox.YLength):
if maxlen < self.stock.BoundBox.YLength:
maxlen = self.stock.BoundBox.YLength
self.voxSim.BeginSimulation(self.stock, 0.01 * self.accuracy * maxlen)
(self.cutMaterial.Mesh, self.cutMaterialIn.Mesh) = self.voxSim.GetResultMesh()
(
self.cutMaterial.Mesh,
self.cutMaterialIn.Mesh,
) = self.voxSim.GetResultMesh()
else:
self.cutMaterial.Shape = self.stock
self.busy = False
self.tool = None
for i in range(len(self.activeOps)):
self.SetupOperation(0)
if (self.tool is not None):
if self.tool is not None:
break
self.iprogress = 0
self.UpdateProgress()
@@ -179,18 +188,29 @@ class PathSimulation:
except Exception:
self.tool = None
if (self.tool is not None):
if self.tool is not None:
if isinstance(self.tool, Path.Tool):
# handle legacy tools
toolProf = self.CreateToolProfile(self.tool, Vector(0, 1, 0), Vector(0, 0, 0), float(self.tool.Diameter) / 2.0)
self.cutTool.Shape = Part.makeSolid(toolProf.revolve(Vector(0, 0, 0), Vector(0, 0, 1)))
toolProf = self.CreateToolProfile(
self.tool,
Vector(0, 1, 0),
Vector(0, 0, 0),
float(self.tool.Diameter) / 2.0,
)
self.cutTool.Shape = Part.makeSolid(
toolProf.revolve(Vector(0, 0, 0), Vector(0, 0, 1))
)
else:
# handle tool bits
self.cutTool.Shape = self.tool.Shape
if not self.cutTool.Shape.isValid() or self.cutTool.Shape.isNull():
self.EndSimulation()
raise RuntimeError("Path Simulation: Error in tool geometry - {}".format(self.tool.Name))
raise RuntimeError(
"Path Simulation: Error in tool geometry - {}".format(
self.tool.Name
)
)
self.cutTool.ViewObject.show()
self.voxSim.SetToolShape(self.cutTool.Shape, 0.05 * self.accuracy)
@@ -206,19 +226,27 @@ class PathSimulation:
self.skipStep = False
self.initialPos = Vector(0, 0, self.job.Stock.Shape.BoundBox.ZMax)
# Add cut tool
self.cutTool = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "CutTool")
self.cutTool = FreeCAD.ActiveDocument.addObject(
"Part::FeaturePython", "CutTool"
)
self.cutTool.ViewObject.Proxy = 0
self.cutTool.ViewObject.hide()
# Add cut material
if self.isVoxel:
self.cutMaterial = FreeCAD.ActiveDocument.addObject("Mesh::FeaturePython", "CutMaterial")
self.cutMaterialIn = FreeCAD.ActiveDocument.addObject("Mesh::FeaturePython", "CutMaterialIn")
self.cutMaterial = FreeCAD.ActiveDocument.addObject(
"Mesh::FeaturePython", "CutMaterial"
)
self.cutMaterialIn = FreeCAD.ActiveDocument.addObject(
"Mesh::FeaturePython", "CutMaterialIn"
)
self.cutMaterialIn.ViewObject.Proxy = 0
self.cutMaterialIn.ViewObject.show()
self.cutMaterialIn.ViewObject.ShapeColor = (1.0, 0.85, 0.45, 0.0)
else:
self.cutMaterial = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "CutMaterial")
self.cutMaterial = FreeCAD.ActiveDocument.addObject(
"Part::FeaturePython", "CutMaterial"
)
self.cutMaterial.Shape = self.job.Stock.Shape
self.cutMaterial.ViewObject.Proxy = 0
self.cutMaterial.ViewObject.show()
@@ -226,7 +254,9 @@ class PathSimulation:
# Add cut path solid for debug
if self.debug:
self.cutSolid = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "CutDebug")
self.cutSolid = FreeCAD.ActiveDocument.addObject(
"Part::FeaturePython", "CutDebug"
)
self.cutSolid.ViewObject.Proxy = 0
self.cutSolid.ViewObject.hide()
@@ -246,28 +276,30 @@ class PathSimulation:
cmd = self.operation.Path.Commands[self.icmd]
pathSolid = None
if cmd.Name in ['G0']:
if cmd.Name in ["G0"]:
self.firstDrill = True
self.curpos = self.RapidMove(cmd, self.curpos)
if cmd.Name in ['G1', 'G2', 'G3']:
if cmd.Name in ["G1", "G2", "G3"]:
self.firstDrill = True
if self.skipStep:
self.curpos = self.RapidMove(cmd, self.curpos)
else:
(pathSolid, self.curpos) = self.GetPathSolid(self.tool, cmd, self.curpos)
(pathSolid, self.curpos) = self.GetPathSolid(
self.tool, cmd, self.curpos
)
if cmd.Name in ['G80']:
if cmd.Name in ["G80"]:
self.firstDrill = True
if cmd.Name in ['G81', 'G82', 'G83']:
if cmd.Name in ["G81", "G82", "G83"]:
if self.firstDrill:
extendcommand = Path.Command('G0', {"Z": cmd.r})
extendcommand = Path.Command("G0", {"Z": cmd.r})
self.curpos = self.RapidMove(extendcommand, self.curpos)
self.firstDrill = False
extendcommand = Path.Command('G0', {"X": cmd.x, "Y": cmd.y, "Z": cmd.r})
extendcommand = Path.Command("G0", {"X": cmd.x, "Y": cmd.y, "Z": cmd.r})
self.curpos = self.RapidMove(extendcommand, self.curpos)
extendcommand = Path.Command('G1', {"X": cmd.x, "Y": cmd.y, "Z": cmd.z})
extendcommand = Path.Command("G1", {"X": cmd.x, "Y": cmd.y, "Z": cmd.z})
self.curpos = self.RapidMove(extendcommand, self.curpos)
extendcommand = Path.Command('G1', {"X": cmd.x, "Y": cmd.y, "Z": cmd.r})
extendcommand = Path.Command("G1", {"X": cmd.x, "Y": cmd.y, "Z": cmd.r})
self.curpos = self.RapidMove(extendcommand, self.curpos)
self.skipStep = False
if pathSolid is not None:
@@ -279,7 +311,9 @@ class PathSimulation:
self.stock = newStock.removeSplitter()
except Exception:
if self.debug:
print("invalid cut at cmd #{}".format(self.icmd))
FreeCAD.Console.PrintError(
"invalid cut at cmd #{}".format(self.icmd)
)
if not self.disableAnim:
self.cutTool.Placement = FreeCAD.Placement(self.curpos, self.stdrot)
self.icmd += 1
@@ -307,27 +341,39 @@ class PathSimulation:
cmd = self.opCommands[self.icmd]
# for cmd in job.Path.Commands:
if cmd.Name in ['G0', 'G1', 'G2', 'G3']:
if cmd.Name in ["G0", "G1", "G2", "G3"]:
self.firstDrill = True
self.curpos = self.voxSim.ApplyCommand(self.curpos, cmd)
if not self.disableAnim:
self.cutTool.Placement = self.curpos
(self.cutMaterial.Mesh, self.cutMaterialIn.Mesh) = self.voxSim.GetResultMesh()
if cmd.Name in ['G80']:
(
self.cutMaterial.Mesh,
self.cutMaterialIn.Mesh,
) = self.voxSim.GetResultMesh()
if cmd.Name in ["G80"]:
self.firstDrill = True
if cmd.Name in ['G81', 'G82', 'G83']:
if cmd.Name in ["G81", "G82", "G83"]:
extendcommands = []
if self.firstDrill:
extendcommands.append(Path.Command('G0', {"Z": cmd.r}))
extendcommands.append(Path.Command("G0", {"Z": cmd.r}))
self.firstDrill = False
extendcommands.append(Path.Command('G0', {"X": cmd.x, "Y": cmd.y, "Z": cmd.r}))
extendcommands.append(Path.Command('G1', {"X": cmd.x, "Y": cmd.y, "Z": cmd.z}))
extendcommands.append(Path.Command('G1', {"X": cmd.x, "Y": cmd.y, "Z": cmd.r}))
extendcommands.append(
Path.Command("G0", {"X": cmd.x, "Y": cmd.y, "Z": cmd.r})
)
extendcommands.append(
Path.Command("G1", {"X": cmd.x, "Y": cmd.y, "Z": cmd.z})
)
extendcommands.append(
Path.Command("G1", {"X": cmd.x, "Y": cmd.y, "Z": cmd.r})
)
for ecmd in extendcommands:
self.curpos = self.voxSim.ApplyCommand(self.curpos, ecmd)
if not self.disableAnim:
self.cutTool.Placement = self.curpos
(self.cutMaterial.Mesh, self.cutMaterialIn.Mesh) = self.voxSim.GetResultMesh()
(
self.cutMaterial.Mesh,
self.cutMaterialIn.Mesh,
) = self.voxSim.GetResultMesh()
self.icmd += 1
self.iprogress += 1
self.UpdateProgress()
@@ -341,7 +387,7 @@ class PathSimulation:
self.busy = False
def PerformCut(self):
if (self.isVoxel):
if self.isVoxel:
self.PerformCutVoxel()
else:
self.PerformCutBoolean()
@@ -457,7 +503,7 @@ class PathSimulation:
form.listOperations.clear()
self.operations = []
for op in j.Operations.OutList:
if PathUtil.opProperty(op, 'Active'):
if PathUtil.opProperty(op, "Active"):
listItem = QtGui.QListWidgetItem(op.ViewObject.Icon, op.Label)
listItem.setFlags(listItem.flags() | QtCore.Qt.ItemIsUserCheckable)
listItem.setCheckState(QtCore.Qt.CheckState.Checked)
@@ -499,7 +545,7 @@ class PathSimulation:
def InvalidOperation(self):
if len(self.activeOps) == 0:
return True
if (self.tool is None):
if self.tool is None:
TSError("No tool assigned for the operation")
return True
return False
@@ -526,7 +572,10 @@ class PathSimulation:
def ViewShape(self):
if self.isVoxel:
(self.cutMaterial.Mesh, self.cutMaterialIn.Mesh) = self.voxSim.GetResultMesh()
(
self.cutMaterial.Mesh,
self.cutMaterialIn.Mesh,
) = self.voxSim.GetResultMesh()
else:
self.cutMaterial.Shape = self.stock
@@ -570,12 +619,15 @@ class PathSimulation:
class CommandPathSimulate:
def GetResources(self):
return {'Pixmap': 'Path_Simulator',
'MenuText': QtCore.QT_TRANSLATE_NOOP("Path_Simulator", "CAM Simulator"),
'Accel': "P, M",
'ToolTip': QtCore.QT_TRANSLATE_NOOP("Path_Simulator", "Simulate Path G-Code on stock")}
return {
"Pixmap": "Path_Simulator",
"MenuText": QT_TRANSLATE_NOOP("Path_Simulator", "CAM Simulator"),
"Accel": "P, M",
"ToolTip": QT_TRANSLATE_NOOP(
"Path_Simulator", "Simulate Path G-Code on stock"
),
}
def IsActive(self):
if FreeCAD.ActiveDocument is not None:
@@ -592,5 +644,5 @@ pathSimulation = PathSimulation()
if FreeCAD.GuiUp:
# register the FreeCAD command
FreeCADGui.addCommand('Path_Simulator', CommandPathSimulate())
FreeCADGui.addCommand("Path_Simulator", CommandPathSimulate())
FreeCAD.Console.PrintLog("Loading PathSimulator Gui... done\n")

View File

@@ -20,51 +20,55 @@
# * *
# ***************************************************************************
'''Used to create material stock around a machined part - for visualization'''
"""Used to create material stock around a machined part - for visualization"""
import FreeCAD
import PathScripts.PathLog as PathLog
import math
from PySide.QtCore import QT_TRANSLATE_NOOP
from PySide import QtCore
# lazily loaded modules
from lazy_loader.lazy_loader import LazyLoader
Part = LazyLoader('Part', globals(), 'Part')
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
#PathLog.trackModule(PathLog.thisModule())
Part = LazyLoader("Part", globals(), "Part")
translate = FreeCAD.Qt.translate
if False:
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
PathLog.trackModule(PathLog.thisModule())
else:
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
# Qt translation handling
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
class StockType:
# pylint: disable=no-init
NoStock = 'None'
FromBase = 'FromBase'
CreateBox = 'CreateBox'
CreateCylinder = 'CreateCylinder'
Unknown = 'Unknown'
NoStock = "None"
FromBase = "FromBase"
CreateBox = "CreateBox"
CreateCylinder = "CreateCylinder"
Unknown = "Unknown"
@classmethod
def FromStock(cls, stock):
'''FromStock(stock) ... Answer a string representing the type of stock.'''
"""FromStock(stock) ... Answer a string representing the type of stock."""
if not stock:
return cls.NoStock
if hasattr(stock, 'StockType'):
if hasattr(stock, "StockType"):
return stock.StockType
# fallback in case somebody messed with internals
if hasattr(stock, 'ExtXneg') and hasattr(stock, 'ExtZpos'):
if hasattr(stock, "ExtXneg") and hasattr(stock, "ExtZpos"):
return cls.FromBase
if hasattr(stock, 'Length') and hasattr(stock, 'Width'):
if hasattr(stock, "Length") and hasattr(stock, "Width"):
return cls.CreateBox
if hasattr(stock, 'Radius') and hasattr(stock, 'Height'):
if hasattr(stock, "Radius") and hasattr(stock, "Height"):
return cls.CreateCylinder
return cls.Unknown
def shapeBoundBox(obj):
PathLog.track(type(obj))
if list == type(obj) and obj:
@@ -73,9 +77,9 @@ def shapeBoundBox(obj):
bb.add(shapeBoundBox(o))
return bb
if hasattr(obj, 'Shape'):
if hasattr(obj, "Shape"):
return obj.Shape.BoundBox
if obj and 'App::Part' == obj.TypeId:
if obj and "App::Part" == obj.TypeId:
bounds = [shapeBoundBox(o) for o in obj.Group]
if bounds:
bb = bounds[0]
@@ -83,39 +87,104 @@ def shapeBoundBox(obj):
bb = bb.united(b)
return bb
if obj:
PathLog.error(translate('PathStock', "Invalid base object %s - no shape found") % obj.Name)
PathLog.error(
translate("PathStock", "Invalid base object %s - no shape found") % obj.Name
)
return None
class Stock(object):
def onDocumentRestored(self, obj):
if hasattr(obj, 'StockType'):
obj.setEditorMode('StockType', 2) # hide
if hasattr(obj, "StockType"):
obj.setEditorMode("StockType", 2) # hide
class StockFromBase(Stock):
def __init__(self, obj, base):
"Make stock"
obj.addProperty("App::PropertyLink", "Base", "Base", QtCore.QT_TRANSLATE_NOOP("PathStock", "The base object this stock is derived from"))
obj.addProperty("App::PropertyDistance", "ExtXneg", "Stock", QtCore.QT_TRANSLATE_NOOP("PathStock", "Extra allowance from part bound box in negative X direction"))
obj.addProperty("App::PropertyDistance", "ExtXpos", "Stock", QtCore.QT_TRANSLATE_NOOP("PathStock", "Extra allowance from part bound box in positive X direction"))
obj.addProperty("App::PropertyDistance", "ExtYneg", "Stock", QtCore.QT_TRANSLATE_NOOP("PathStock", "Extra allowance from part bound box in negative Y direction"))
obj.addProperty("App::PropertyDistance", "ExtYpos", "Stock", QtCore.QT_TRANSLATE_NOOP("PathStock", "Extra allowance from part bound box in positive Y direction"))
obj.addProperty("App::PropertyDistance", "ExtZneg", "Stock", QtCore.QT_TRANSLATE_NOOP("PathStock", "Extra allowance from part bound box in negative Z direction"))
obj.addProperty("App::PropertyDistance", "ExtZpos", "Stock", QtCore.QT_TRANSLATE_NOOP("PathStock", "Extra allowance from part bound box in positive Z direction"))
obj.addProperty("App::PropertyLink","Material","Component", QtCore.QT_TRANSLATE_NOOP("App::Property","A material for this object"))
obj.addProperty(
"App::PropertyLink",
"Base",
"Base",
QT_TRANSLATE_NOOP(
"App::Property", "The base object this stock is derived from"
),
)
obj.addProperty(
"App::PropertyDistance",
"ExtXneg",
"Stock",
QT_TRANSLATE_NOOP(
"App::Property",
"Extra allowance from part bound box in negative X direction",
),
)
obj.addProperty(
"App::PropertyDistance",
"ExtXpos",
"Stock",
QT_TRANSLATE_NOOP(
"App::Property",
"Extra allowance from part bound box in positive X direction",
),
)
obj.addProperty(
"App::PropertyDistance",
"ExtYneg",
"Stock",
QT_TRANSLATE_NOOP(
"App::Property",
"Extra allowance from part bound box in negative Y direction",
),
)
obj.addProperty(
"App::PropertyDistance",
"ExtYpos",
"Stock",
QT_TRANSLATE_NOOP(
"App::Property",
"Extra allowance from part bound box in positive Y direction",
),
)
obj.addProperty(
"App::PropertyDistance",
"ExtZneg",
"Stock",
QT_TRANSLATE_NOOP(
"App::Property",
"Extra allowance from part bound box in negative Z direction",
),
)
obj.addProperty(
"App::PropertyDistance",
"ExtZpos",
"Stock",
QT_TRANSLATE_NOOP(
"App::Property",
"Extra allowance from part bound box in positive Z direction",
),
)
obj.addProperty(
"App::PropertyLink",
"Material",
"Component",
QT_TRANSLATE_NOOP("App::Property", "A material for this object"),
)
obj.Base = base
obj.ExtXneg= 1.0
obj.ExtXpos= 1.0
obj.ExtYneg= 1.0
obj.ExtYpos= 1.0
obj.ExtZneg= 1.0
obj.ExtZpos= 1.0
obj.ExtXneg = 1.0
obj.ExtXpos = 1.0
obj.ExtYneg = 1.0
obj.ExtYpos = 1.0
obj.ExtZneg = 1.0
obj.ExtZpos = 1.0
# placement is only tracked on creation
bb = shapeBoundBox(base.Group) if base else None
if bb:
obj.Placement = FreeCAD.Placement(FreeCAD.Vector(bb.XMin, bb.YMin, bb.ZMin), FreeCAD.Rotation())
obj.Placement = FreeCAD.Placement(
FreeCAD.Vector(bb.XMin, bb.YMin, bb.ZMin), FreeCAD.Rotation()
)
else:
PathLog.track(obj.Label, base.Label)
obj.Proxy = self
@@ -123,25 +192,32 @@ class StockFromBase(Stock):
# debugging aids
self.origin = None
self.length = None
self.width = None
self.width = None
self.height = None
def __getstate__(self):
return None
def __setstate__(self, state):
return None
def execute(self, obj):
bb = shapeBoundBox(obj.Base.Group) if obj.Base and hasattr(obj.Base, 'Group') else None
bb = (
shapeBoundBox(obj.Base.Group)
if obj.Base and hasattr(obj.Base, "Group")
else None
)
PathLog.track(obj.Label, bb)
# Sometimes, when the Base changes it's temporarily not assigned when
# Stock.execute is triggered - it'll be set correctly the next time around.
if bb:
self.origin = FreeCAD.Vector(-obj.ExtXneg.Value, -obj.ExtYneg.Value, -obj.ExtZneg.Value)
self.origin = FreeCAD.Vector(
-obj.ExtXneg.Value, -obj.ExtYneg.Value, -obj.ExtZneg.Value
)
self.length = bb.XLength + obj.ExtXneg.Value + obj.ExtXpos.Value
self.width = bb.YLength + obj.ExtYneg.Value + obj.ExtYpos.Value
self.width = bb.YLength + obj.ExtYneg.Value + obj.ExtYpos.Value
self.height = bb.ZLength + obj.ExtZneg.Value + obj.ExtZpos.Value
shape = Part.makeBox(self.length, self.width, self.height, self.origin)
@@ -149,7 +225,10 @@ class StockFromBase(Stock):
obj.Shape = shape
def onChanged(self, obj, prop):
if prop in ['ExtXneg', 'ExtXpos', 'ExtYneg', 'ExtYpos', 'ExtZneg', 'ExtZpos'] and not 'Restore' in obj.State:
if (
prop in ["ExtXneg", "ExtXpos", "ExtYneg", "ExtYpos", "ExtZneg", "ExtZpos"]
and not "Restore" in obj.State
):
self.execute(obj)
@@ -157,18 +236,34 @@ class StockCreateBox(Stock):
MinExtent = 0.001
def __init__(self, obj):
obj.addProperty('App::PropertyLength', 'Length', 'Stock', QtCore.QT_TRANSLATE_NOOP("PathStock", "Length of this stock box"))
obj.addProperty('App::PropertyLength', 'Width', 'Stock', QtCore.QT_TRANSLATE_NOOP("PathStock", "Width of this stock box"))
obj.addProperty('App::PropertyLength', 'Height', 'Stock', QtCore.QT_TRANSLATE_NOOP("PathStock", "Height of this stock box"))
obj.addProperty(
"App::PropertyLength",
"Length",
"Stock",
QT_TRANSLATE_NOOP("App::Property", "Length of this stock box"),
)
obj.addProperty(
"App::PropertyLength",
"Width",
"Stock",
QT_TRANSLATE_NOOP("App::Property", "Width of this stock box"),
)
obj.addProperty(
"App::PropertyLength",
"Height",
"Stock",
QT_TRANSLATE_NOOP("App::Property", "Height of this stock box"),
)
obj.Length = 10
obj.Width = 10
obj.Width = 10
obj.Height = 10
obj.Proxy = self
def __getstate__(self):
return None
def __setstate__(self, state):
return None
@@ -185,15 +280,26 @@ class StockCreateBox(Stock):
obj.Shape = shape
def onChanged(self, obj, prop):
if prop in ['Length', 'Width', 'Height'] and not 'Restore' in obj.State:
if prop in ["Length", "Width", "Height"] and not "Restore" in obj.State:
self.execute(obj)
class StockCreateCylinder(Stock):
MinExtent = 0.001
def __init__(self, obj):
obj.addProperty('App::PropertyLength', 'Radius', 'Stock', QtCore.QT_TRANSLATE_NOOP("PathStock", "Radius of this stock cylinder"))
obj.addProperty('App::PropertyLength', 'Height', 'Stock', QtCore.QT_TRANSLATE_NOOP("PathStock", "Height of this stock cylinder"))
obj.addProperty(
"App::PropertyLength",
"Radius",
"Stock",
QT_TRANSLATE_NOOP("App::Property", "Radius of this stock cylinder"),
)
obj.addProperty(
"App::PropertyLength",
"Height",
"Stock",
QT_TRANSLATE_NOOP("App::Property", "Height of this stock cylinder"),
)
obj.Radius = 2
obj.Height = 10
@@ -202,6 +308,7 @@ class StockCreateCylinder(Stock):
def __getstate__(self):
return None
def __setstate__(self, state):
return None
@@ -216,38 +323,49 @@ class StockCreateCylinder(Stock):
obj.Shape = shape
def onChanged(self, obj, prop):
if prop in ['Radius', 'Height'] and not 'Restore' in obj.State:
if prop in ["Radius", "Height"] and not "Restore" in obj.State:
self.execute(obj)
def SetupStockObject(obj, stockType):
PathLog.track(obj.Label, stockType)
if FreeCAD.GuiUp and obj.ViewObject:
obj.addProperty('App::PropertyString', 'StockType', 'Stock', QtCore.QT_TRANSLATE_NOOP("PathStock", "Internal representation of stock type"))
obj.addProperty(
"App::PropertyString",
"StockType",
"Stock",
QT_TRANSLATE_NOOP("App::Property", "Internal representation of stock type"),
)
obj.StockType = stockType
obj.setEditorMode('StockType', 2) # hide
obj.setEditorMode("StockType", 2) # hide
import PathScripts.PathIconViewProvider
PathScripts.PathIconViewProvider.ViewProvider(obj.ViewObject, 'Stock')
PathScripts.PathIconViewProvider.ViewProvider(obj.ViewObject, "Stock")
obj.ViewObject.Transparency = 90
obj.ViewObject.DisplayMode = 'Wireframe'
obj.ViewObject.DisplayMode = "Wireframe"
class FakeJob(object):
def __init__(self, base):
self.Group = [base]
def _getBase(job):
if job and hasattr(job, 'Model'):
if job and hasattr(job, "Model"):
return job.Model
if job:
import PathScripts.PathUtils as PathUtils
job = PathUtils.findParentJob(job)
return job.Model if job else None
return None
def CreateFromBase(job, neg=None, pos=None, placement=None):
PathLog.track(job.Label, neg, pos, placement)
base = _getBase(job)
obj = FreeCAD.ActiveDocument.addObject('Part::FeaturePython', 'Stock')
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "Stock")
obj.Proxy = StockFromBase(obj, base)
if neg:
@@ -268,19 +386,20 @@ def CreateFromBase(job, neg=None, pos=None, placement=None):
obj.purgeTouched()
return obj
def CreateBox(job, extent=None, placement=None):
base = _getBase(job)
obj = FreeCAD.ActiveDocument.addObject('Part::FeaturePython', 'Stock')
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "Stock")
obj.Proxy = StockCreateBox(obj)
if extent:
obj.Length = extent.x
obj.Width = extent.y
obj.Width = extent.y
obj.Height = extent.z
elif base:
bb = shapeBoundBox(base.Group)
obj.Length = max(bb.XLength, 1)
obj.Width = max(bb.YLength, 1)
obj.Width = max(bb.YLength, 1)
obj.Height = max(bb.ZLength, 1)
if placement:
@@ -293,9 +412,10 @@ def CreateBox(job, extent=None, placement=None):
SetupStockObject(obj, StockType.CreateBox)
return obj
def CreateCylinder(job, radius=None, height=None, placement=None):
base = _getBase(job)
obj = FreeCAD.ActiveDocument.addObject('Part::FeaturePython', 'Stock')
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "Stock")
obj.Proxy = StockCreateCylinder(obj)
if radius:
@@ -312,112 +432,179 @@ def CreateCylinder(job, radius=None, height=None, placement=None):
obj.Placement = placement
elif base:
bb = shapeBoundBox(base.Group)
origin = FreeCAD.Vector((bb.XMin + bb.XMax)/2, (bb.YMin + bb.YMax)/2, bb.ZMin)
origin = FreeCAD.Vector(
(bb.XMin + bb.XMax) / 2, (bb.YMin + bb.YMax) / 2, bb.ZMin
)
obj.Placement = FreeCAD.Placement(origin, FreeCAD.Vector(), 0)
SetupStockObject(obj, StockType.CreateCylinder)
return obj
def TemplateAttributes(stock, includeExtent=True, includePlacement=True):
attrs = {}
if stock:
attrs['version'] = 1
attrs["version"] = 1
stockType = StockType.FromStock(stock)
attrs['create'] = stockType
attrs["create"] = stockType
if includeExtent:
if stockType == StockType.FromBase:
attrs['xneg'] = ("%s" % stock.ExtXneg)
attrs['xpos'] = ("%s" % stock.ExtXpos)
attrs['yneg'] = ("%s" % stock.ExtYneg)
attrs['ypos'] = ("%s" % stock.ExtYpos)
attrs['zneg'] = ("%s" % stock.ExtZneg)
attrs['zpos'] = ("%s" % stock.ExtZpos)
attrs["xneg"] = "%s" % stock.ExtXneg
attrs["xpos"] = "%s" % stock.ExtXpos
attrs["yneg"] = "%s" % stock.ExtYneg
attrs["ypos"] = "%s" % stock.ExtYpos
attrs["zneg"] = "%s" % stock.ExtZneg
attrs["zpos"] = "%s" % stock.ExtZpos
if stockType == StockType.CreateBox:
attrs['length'] = ("%s" % stock.Length)
attrs['width'] = ("%s" % stock.Width)
attrs['height'] = ("%s" % stock.Height)
attrs["length"] = "%s" % stock.Length
attrs["width"] = "%s" % stock.Width
attrs["height"] = "%s" % stock.Height
if stockType == StockType.CreateCylinder:
attrs['radius'] = ("%s" % stock.Radius)
attrs['height'] = ("%s" % stock.Height)
attrs["radius"] = "%s" % stock.Radius
attrs["height"] = "%s" % stock.Height
if includePlacement:
pos = stock.Placement.Base
attrs['posX'] = pos.x
attrs['posY'] = pos.y
attrs['posZ'] = pos.z
attrs["posX"] = pos.x
attrs["posY"] = pos.y
attrs["posZ"] = pos.z
rot = stock.Placement.Rotation
attrs['rotX'] = rot.Q[0]
attrs['rotY'] = rot.Q[1]
attrs['rotZ'] = rot.Q[2]
attrs['rotW'] = rot.Q[3]
attrs["rotX"] = rot.Q[0]
attrs["rotY"] = rot.Q[1]
attrs["rotZ"] = rot.Q[2]
attrs["rotW"] = rot.Q[3]
return attrs
def CreateFromTemplate(job, template):
if template.get('version') and 1 == int(template['version']):
stockType = template.get('create')
if template.get("version") and 1 == int(template["version"]):
stockType = template.get("create")
if stockType:
placement = None
posX = template.get('posX')
posY = template.get('posY')
posZ = template.get('posZ')
rotX = template.get('rotX')
rotY = template.get('rotY')
rotZ = template.get('rotZ')
rotW = template.get('rotW')
if posX is not None and posY is not None and posZ is not None and rotX is not None and rotY is not None and rotZ is not None and rotW is not None:
posX = template.get("posX")
posY = template.get("posY")
posZ = template.get("posZ")
rotX = template.get("rotX")
rotY = template.get("rotY")
rotZ = template.get("rotZ")
rotW = template.get("rotW")
if (
posX is not None
and posY is not None
and posZ is not None
and rotX is not None
and rotY is not None
and rotZ is not None
and rotW is not None
):
pos = FreeCAD.Vector(float(posX), float(posY), float(posZ))
rot = FreeCAD.Rotation(float(rotX), float(rotY), float(rotZ), float(rotW))
rot = FreeCAD.Rotation(
float(rotX), float(rotY), float(rotZ), float(rotW)
)
placement = FreeCAD.Placement(pos, rot)
elif posX is not None or posY is not None or posZ is not None or rotX is not None or rotY is not None or rotZ is not None or rotW is not None:
PathLog.warning(translate('PathStock', 'Corrupted or incomplete placement information in template - ignoring'))
elif (
posX is not None
or posY is not None
or posZ is not None
or rotX is not None
or rotY is not None
or rotZ is not None
or rotW is not None
):
PathLog.warning(
"Corrupted or incomplete placement information in template - ignoring"
)
if stockType == StockType.FromBase:
xneg = template.get('xneg')
xpos = template.get('xpos')
yneg = template.get('yneg')
ypos = template.get('ypos')
zneg = template.get('zneg')
zpos = template.get('zpos')
xneg = template.get("xneg")
xpos = template.get("xpos")
yneg = template.get("yneg")
ypos = template.get("ypos")
zneg = template.get("zneg")
zpos = template.get("zpos")
neg = None
pos = None
if xneg is not None and xpos is not None and yneg is not None and ypos is not None and zneg is not None and zpos is not None:
neg = FreeCAD.Vector(FreeCAD.Units.Quantity(xneg).Value, FreeCAD.Units.Quantity(yneg).Value, FreeCAD.Units.Quantity(zneg).Value)
pos = FreeCAD.Vector(FreeCAD.Units.Quantity(xpos).Value, FreeCAD.Units.Quantity(ypos).Value, FreeCAD.Units.Quantity(zpos).Value)
elif xneg is not None or xpos is not None or yneg is not None or ypos is not None or zneg is not None or zpos is not None:
PathLog.error(translate('PathStock', 'Corrupted or incomplete specification for creating stock from base - ignoring extent'))
if (
xneg is not None
and xpos is not None
and yneg is not None
and ypos is not None
and zneg is not None
and zpos is not None
):
neg = FreeCAD.Vector(
FreeCAD.Units.Quantity(xneg).Value,
FreeCAD.Units.Quantity(yneg).Value,
FreeCAD.Units.Quantity(zneg).Value,
)
pos = FreeCAD.Vector(
FreeCAD.Units.Quantity(xpos).Value,
FreeCAD.Units.Quantity(ypos).Value,
FreeCAD.Units.Quantity(zpos).Value,
)
elif (
xneg is not None
or xpos is not None
or yneg is not None
or ypos is not None
or zneg is not None
or zpos is not None
):
PathLog.error(
"Corrupted or incomplete specification for creating stock from base - ignoring extent"
)
return CreateFromBase(job, neg, pos, placement)
if stockType == StockType.CreateBox:
PathLog.track(' create box')
length = template.get('length')
width = template.get('width')
height = template.get('height')
PathLog.track(" create box")
length = template.get("length")
width = template.get("width")
height = template.get("height")
extent = None
if length is not None and width is not None and height is not None:
PathLog.track(' have extent')
extent = FreeCAD.Vector(FreeCAD.Units.Quantity(length).Value, FreeCAD.Units.Quantity(width).Value, FreeCAD.Units.Quantity(height).Value)
PathLog.track(" have extent")
extent = FreeCAD.Vector(
FreeCAD.Units.Quantity(length).Value,
FreeCAD.Units.Quantity(width).Value,
FreeCAD.Units.Quantity(height).Value,
)
elif length is not None or width is not None or height is not None:
PathLog.error(translate('PathStock', 'Corrupted or incomplete size for creating a stock box - ignoring size'))
PathLog.error(
"Corrupted or incomplete size for creating a stock box - ignoring size"
)
else:
PathLog.track(" take placement (%s) and extent (%s) from model" % (placement, extent))
PathLog.track(
" take placement (%s) and extent (%s) from model"
% (placement, extent)
)
return CreateBox(job, extent, placement)
if stockType == StockType.CreateCylinder:
radius = template.get('radius')
height = template.get('height')
radius = template.get("radius")
height = template.get("height")
if radius is not None and height is not None:
pass
elif radius is not None or height is not None:
radius = None
height = None
PathLog.error(translate('PathStock', 'Corrupted or incomplete size for creating a stock cylinder - ignoring size'))
PathLog.error(
"Corrupted or incomplete size for creating a stock cylinder - ignoring size"
)
return CreateCylinder(job, radius, height, placement)
PathLog.error(translate('PathStock', 'Unsupported stock type named {}').format(stockType))
PathLog.error(
translate("PathStock", "Unsupported stock type named {}").format(
stockType
)
)
else:
PathLog.error(translate('PathStock', 'Unsupported PathStock template version {}').format(template.get('version')))
PathLog.error(
translate(
"PathStock", "Unsupported PathStock template version {}"
).format(template.get("version"))
)
return None

View File

@@ -20,24 +20,31 @@
# * *
# ***************************************************************************
'''Used for CNC machine Stops for Path module. Create an Optional or Mandatory Stop.'''
"""Used for CNC machine Stops for Path module. Create an Optional or Mandatory Stop."""
import FreeCAD
import FreeCADGui
import Path
from PySide import QtCore
from PySide.QtCore import QT_TRANSLATE_NOOP
translate = FreeCAD.Qt.translate
# Qt translation handling
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
class Stop:
def __init__(self,obj):
obj.addProperty("App::PropertyEnumeration", "Stop", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property","Add Optional or Mandatory Stop to the program"))
obj.Stop=['Optional', 'Mandatory']
def __init__(self, obj):
obj.addProperty(
"App::PropertyEnumeration",
"Stop",
"Path",
QT_TRANSLATE_NOOP(
"App::Property", "Add Optional or Mandatory Stop to the program"
),
)
obj.Stop = ["Optional", "Mandatory"]
obj.Proxy = self
mode = 2
obj.setEditorMode('Placement', mode)
obj.setEditorMode("Placement", mode)
def __getstate__(self):
return None
@@ -49,31 +56,30 @@ class Stop:
pass
def execute(self, obj):
if obj.Stop == 'Optional':
word = 'M1'
if obj.Stop == "Optional":
word = "M1"
else:
word = 'M0'
word = "M0"
output = ""
output = word + '\n'
output = word + "\n"
path = Path.Path(output)
obj.Path = path
class _ViewProviderStop:
def __init__(self, vobj): # mandatory
vobj.Proxy = self
mode = 2
vobj.setEditorMode('LineWidth', mode)
vobj.setEditorMode('MarkerColor', mode)
vobj.setEditorMode('NormalColor', mode)
vobj.setEditorMode('DisplayMode', mode)
vobj.setEditorMode('BoundingBox', mode)
vobj.setEditorMode('Selectable', mode)
vobj.setEditorMode('ShapeColor', mode)
vobj.setEditorMode('Transparency', mode)
vobj.setEditorMode('Visibility', mode)
vobj.setEditorMode("LineWidth", mode)
vobj.setEditorMode("MarkerColor", mode)
vobj.setEditorMode("NormalColor", mode)
vobj.setEditorMode("DisplayMode", mode)
vobj.setEditorMode("BoundingBox", mode)
vobj.setEditorMode("Selectable", mode)
vobj.setEditorMode("ShapeColor", mode)
vobj.setEditorMode("Transparency", mode)
vobj.setEditorMode("Visibility", mode)
def __getstate__(self): # mandatory
return None
@@ -87,23 +93,26 @@ class _ViewProviderStop:
def onChanged(self, vobj, prop): # optional
# pylint: disable=unused-argument
mode = 2
vobj.setEditorMode('LineWidth', mode)
vobj.setEditorMode('MarkerColor', mode)
vobj.setEditorMode('NormalColor', mode)
vobj.setEditorMode('DisplayMode', mode)
vobj.setEditorMode('BoundingBox', mode)
vobj.setEditorMode('Selectable', mode)
vobj.setEditorMode('ShapeColor', mode)
vobj.setEditorMode('Transparency', mode)
vobj.setEditorMode('Visibility', mode)
vobj.setEditorMode("LineWidth", mode)
vobj.setEditorMode("MarkerColor", mode)
vobj.setEditorMode("NormalColor", mode)
vobj.setEditorMode("DisplayMode", mode)
vobj.setEditorMode("BoundingBox", mode)
vobj.setEditorMode("Selectable", mode)
vobj.setEditorMode("ShapeColor", mode)
vobj.setEditorMode("Transparency", mode)
vobj.setEditorMode("Visibility", mode)
class CommandPathStop:
def GetResources(self):
return {'Pixmap': 'Path_Stop',
'MenuText': QtCore.QT_TRANSLATE_NOOP("Path_Stop", "Stop"),
'ToolTip': QtCore.QT_TRANSLATE_NOOP("Path_Stop", "Add Optional or Mandatory Stop to the program")}
return {
"Pixmap": "Path_Stop",
"MenuText": QT_TRANSLATE_NOOP("Path_Stop", "Stop"),
"ToolTip": QT_TRANSLATE_NOOP(
"Path_Stop", "Add Optional or Mandatory Stop to the program"
),
}
def IsActive(self):
if FreeCAD.ActiveDocument is not None:
@@ -114,9 +123,10 @@ class CommandPathStop:
def Activated(self):
FreeCAD.ActiveDocument.openTransaction(
translate("Path_Stop", "Add Optional or Mandatory Stop to the program"))
"Add Optional or Mandatory Stop to the program"
)
FreeCADGui.addModule("PathScripts.PathStop")
snippet = '''
snippet = """
import Path
import PathScripts
from PathScripts import PathUtils
@@ -126,14 +136,15 @@ PathScripts.PathStop.Stop(obj)
PathScripts.PathStop._ViewProviderStop(obj.ViewObject)
PathUtils.addToJob(obj)
'''
"""
FreeCADGui.doCommand(snippet)
FreeCAD.ActiveDocument.commitTransaction()
FreeCAD.ActiveDocument.recompute()
if FreeCAD.GuiUp:
# register the FreeCAD command
FreeCADGui.addCommand('Path_Stop', CommandPathStop())
FreeCADGui.addCommand("Path_Stop", CommandPathStop())
FreeCAD.Console.PrintLog("Loading PathStop... done\n")

View File

@@ -20,7 +20,7 @@
# * *
# ***************************************************************************
'''
"""
The purpose of this file is to collect some handy functions. The reason they
are not in PathUtils (and there is this confusing naming going on) is that
PathUtils depends on PathJob. Which makes it impossible to use the functions
@@ -28,125 +28,148 @@ and classes defined there in PathJob.
So if you add to this file and think about importing anything from PathScripts
other than PathLog, then it probably doesn't belong here.
'''
"""
import FreeCAD
import six
import PathScripts.PathLog as PathLog
import PySide
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
translate = FreeCAD.Qt.translate
if False:
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
PathLog.trackModule(PathLog.thisModule())
else:
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
def translate(context, text, disambig=None):
return PySide.QtCore.QCoreApplication.translate(context, text, disambig)
def _getProperty(obj, prop):
o = obj
attr = obj
name = None
for name in prop.split('.'):
for name in prop.split("."):
o = attr
if not hasattr(o, name):
break
attr = getattr(o, name)
if o == attr:
PathLog.warning(translate('PathGui', "%s has no property %s (%s))") % (obj.Label, prop, name))
PathLog.warning(
translate("PathGui", "%s has no property %s (%s))")
% (obj.Label, prop, name)
)
return (None, None, None)
#PathLog.debug("found property %s of %s (%s: %s)" % (prop, obj.Label, name, attr))
return(o, attr, name)
# PathLog.debug("found property %s of %s (%s: %s)" % (prop, obj.Label, name, attr))
return (o, attr, name)
def getProperty(obj, prop):
'''getProperty(obj, prop) ... answer obj's property defined by its canonical name.'''
o, attr, name = _getProperty(obj, prop) # pylint: disable=unused-variable
"""getProperty(obj, prop) ... answer obj's property defined by its canonical name."""
o, attr, name = _getProperty(obj, prop) # pylint: disable=unused-variable
return attr
def getPropertyValueString(obj, prop):
'''getPropertyValueString(obj, prop) ... answer a string representation of an object's property's value.'''
"""getPropertyValueString(obj, prop) ... answer a string representation of an object's property's value."""
attr = getProperty(obj, prop)
if hasattr(attr, 'UserString'):
if hasattr(attr, "UserString"):
return attr.UserString
return str(attr)
def setProperty(obj, prop, value):
'''setProperty(obj, prop, value) ... set the property value of obj's property defined by its canonical name.'''
o, attr, name = _getProperty(obj, prop) # pylint: disable=unused-variable
"""setProperty(obj, prop, value) ... set the property value of obj's property defined by its canonical name."""
o, attr, name = _getProperty(obj, prop) # pylint: disable=unused-variable
if not attr is None and type(value) == str:
if type(attr) == int:
value = int(value, 0)
elif type(attr) == bool:
value = value.lower() in ['true', '1', 'yes', 'ok']
value = value.lower() in ["true", "1", "yes", "ok"]
if o and name:
setattr(o, name, value)
# NotValidBaseTypeIds = ['Sketcher::SketchObject']
NotValidBaseTypeIds = []
def isValidBaseObject(obj):
'''isValidBaseObject(obj) ... returns true if the object can be used as a base for a job.'''
if hasattr(obj, 'getParentGeoFeatureGroup') and obj.getParentGeoFeatureGroup():
"""isValidBaseObject(obj) ... returns true if the object can be used as a base for a job."""
if hasattr(obj, "getParentGeoFeatureGroup") and obj.getParentGeoFeatureGroup():
# Can't link to anything inside a geo feature group anymore
PathLog.debug("%s is inside a geo feature group" % obj.Label)
return False
if hasattr(obj, 'BitBody') and hasattr(obj, 'BitShape'):
if hasattr(obj, "BitBody") and hasattr(obj, "BitShape"):
# ToolBit's are not valid base objects
return False
if obj.TypeId in NotValidBaseTypeIds:
PathLog.debug("%s is blacklisted (%s)" % (obj.Label, obj.TypeId))
return False
if hasattr(obj, 'Sheets') or hasattr(obj, 'TagText'): # Arch.Panels and Arch.PanelCut
if hasattr(obj, "Sheets") or hasattr(
obj, "TagText"
): # Arch.Panels and Arch.PanelCut
PathLog.debug("%s is not an Arch.Panel" % (obj.Label))
return False
import Part
return not Part.getShape(obj).isNull()
def isSolid(obj):
'''isSolid(obj) ... return True if the object is a valid solid.'''
"""isSolid(obj) ... return True if the object is a valid solid."""
import Part
shape = Part.getShape(obj)
return not shape.isNull() and shape.Volume and shape.isClosed()
def opProperty(op, prop):
'''opProperty(op, prop) ... return the value of property prop of the underlying operation (or None if prop does not exist)'''
"""opProperty(op, prop) ... return the value of property prop of the underlying operation (or None if prop does not exist)"""
if hasattr(op, prop):
return getattr(op, prop)
if hasattr(op, 'Base'):
if hasattr(op, "Base"):
return opProperty(op.Base, prop)
return None
def toolControllerForOp(op):
'''toolControllerForOp(op) ... return the tool controller used by the op.
"""toolControllerForOp(op) ... return the tool controller used by the op.
If the op doesn't have its own tool controller but has a Base object, return its tool controller.
Otherwise return None.'''
return opProperty(op, 'ToolController')
Otherwise return None."""
return opProperty(op, "ToolController")
def getPublicObject(obj):
'''getPublicObject(obj) ... returns the object which should be used to reference a feature of the given object.'''
if hasattr(obj, 'getParentGeoFeatureGroup'):
"""getPublicObject(obj) ... returns the object which should be used to reference a feature of the given object."""
if hasattr(obj, "getParentGeoFeatureGroup"):
body = obj.getParentGeoFeatureGroup()
if body:
return getPublicObject(body)
return obj
def clearExpressionEngine(obj):
'''clearExpressionEngine(obj) ... removes all expressions from obj.
There is currently a bug that invalidates the DAG if an object
is deleted that still has one or more expressions attached to it.
Use this function to remove all expressions before deletion.'''
if hasattr(obj, 'ExpressionEngine'):
for attr, expr in obj.ExpressionEngine: # pylint: disable=unused-variable
def clearExpressionEngine(obj):
"""clearExpressionEngine(obj) ... removes all expressions from obj.
There is currently a bug that invalidates the DAG if an object
is deleted that still has one or more expressions attached to it.
Use this function to remove all expressions before deletion."""
if hasattr(obj, "ExpressionEngine"):
for attr, expr in obj.ExpressionEngine: # pylint: disable=unused-variable
obj.setExpression(attr, None)
def toUnicode(string):
'''toUnicode(string) ... returns a unicode version of string regardless of the python version.'''
"""toUnicode(string) ... returns a unicode version of string regardless of the python version."""
return six.text_type(string)
def isString(string):
'''isString(string) ... return True if string is a string, regardless of string type and python version.'''
"""isString(string) ... return True if string is a string, regardless of string type and python version."""
return isinstance(string, six.string_types)
def keyValueIter(dictionary):
'''keyValueIter(dict) ... return iterable object over dictionary's (key,value) tuples.'''
"""keyValueIter(dict) ... return iterable object over dictionary's (key,value) tuples."""
return six.iteritems(dictionary)

View File

@@ -19,9 +19,10 @@
# * USA *
# * *
# ***************************************************************************
'''PathUtils -common functions used in PathScripts for filtering, sorting, and generating gcode toolpath data '''
"""PathUtils -common functions used in PathScripts for filtering, sorting, and generating gcode toolpath data """
import FreeCAD
import Path
# import PathScripts
import PathScripts.PathJob as PathJob
import PathScripts.PathGeom as PathGeom
@@ -35,16 +36,19 @@ from PySide import QtGui
# lazily loaded modules
from lazy_loader.lazy_loader import LazyLoader
DraftGeomUtils = LazyLoader('DraftGeomUtils', globals(), 'DraftGeomUtils')
Part = LazyLoader('Part', globals(), 'Part')
TechDraw = LazyLoader('TechDraw', globals(), 'TechDraw')
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
# PathLog.trackModule(PathLog.thisModule())
DraftGeomUtils = LazyLoader("DraftGeomUtils", globals(), "DraftGeomUtils")
Part = LazyLoader("Part", globals(), "Part")
TechDraw = LazyLoader("TechDraw", globals(), "TechDraw")
translate = FreeCAD.Qt.translate
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
if False:
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
PathLog.trackModule(PathLog.thisModule())
else:
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
UserInput = None
@@ -65,6 +69,7 @@ def waiting_effects(function):
finally:
QtGui.QApplication.restoreOverrideCursor()
return res
return new_function
@@ -78,7 +83,9 @@ def isDrillable(obj, candidate, tooldiameter=None, includePartials=False):
candidate = Face or Edge
tooldiameter=float
"""
PathLog.track('obj: {} candidate: {} tooldiameter {}'.format(obj, candidate, tooldiameter))
PathLog.track(
"obj: {} candidate: {} tooldiameter {}".format(obj, candidate, tooldiameter)
)
if list == type(obj):
for shape in obj:
if isDrillable(shape, candidate, tooldiameter, includePartials):
@@ -87,28 +94,47 @@ def isDrillable(obj, candidate, tooldiameter=None, includePartials=False):
drillable = False
try:
if candidate.ShapeType == 'Face':
if candidate.ShapeType == "Face":
face = candidate
# eliminate flat faces
if (round(face.ParameterRange[0], 8) == 0.0) and (round(face.ParameterRange[1], 8) == round(math.pi * 2, 8)):
for edge in face.Edges: # Find seam edge and check if aligned to Z axis.
if (isinstance(edge.Curve, Part.Line)):
if (round(face.ParameterRange[0], 8) == 0.0) and (
round(face.ParameterRange[1], 8) == round(math.pi * 2, 8)
):
for (
edge
) in face.Edges: # Find seam edge and check if aligned to Z axis.
if isinstance(edge.Curve, Part.Line):
PathLog.debug("candidate is a circle")
v0 = edge.Vertexes[0].Point
v1 = edge.Vertexes[1].Point
# check if the cylinder seam is vertically aligned. Eliminate tilted holes
if (numpy.isclose(v1.sub(v0).x, 0, rtol=1e-05, atol=1e-06)) and \
(numpy.isclose(v1.sub(v0).y, 0, rtol=1e-05, atol=1e-06)):
if (
numpy.isclose(v1.sub(v0).x, 0, rtol=1e-05, atol=1e-06)
) and (numpy.isclose(v1.sub(v0).y, 0, rtol=1e-05, atol=1e-06)):
drillable = True
# vector of top center
lsp = Vector(face.BoundBox.Center.x, face.BoundBox.Center.y, face.BoundBox.ZMax)
lsp = Vector(
face.BoundBox.Center.x,
face.BoundBox.Center.y,
face.BoundBox.ZMax,
)
# vector of bottom center
lep = Vector(face.BoundBox.Center.x, face.BoundBox.Center.y, face.BoundBox.ZMin)
lep = Vector(
face.BoundBox.Center.x,
face.BoundBox.Center.y,
face.BoundBox.ZMin,
)
# check if the cylindrical 'lids' are inside the base
# object. This eliminates extruded circles but allows
# actual holes.
if obj.isInside(lsp, 1e-6, False) or obj.isInside(lep, 1e-6, False):
PathLog.track("inside check failed. lsp: {} lep: {}".format(lsp, lep))
if obj.isInside(lsp, 1e-6, False) or obj.isInside(
lep, 1e-6, False
):
PathLog.track(
"inside check failed. lsp: {} lep: {}".format(
lsp, lep
)
)
drillable = False
# eliminate elliptical holes
elif not hasattr(face.Surface, "Radius"):
@@ -119,7 +145,9 @@ def isDrillable(obj, candidate, tooldiameter=None, includePartials=False):
drillable = face.Surface.Radius >= tooldiameter / 2
else:
drillable = True
elif type(face.Surface) == Part.Plane and PathGeom.pointsCoincide(face.Surface.Axis, FreeCAD.Vector(0, 0, 1)):
elif type(face.Surface) == Part.Plane and PathGeom.pointsCoincide(
face.Surface.Axis, FreeCAD.Vector(0, 0, 1)
):
if len(face.Edges) == 1 and type(face.Edges[0].Curve) == Part.Circle:
center = face.Edges[0].Curve.Center
if obj.isInside(center, 1e-6, False):
@@ -129,7 +157,9 @@ def isDrillable(obj, candidate, tooldiameter=None, includePartials=False):
drillable = True
else:
for edge in candidate.Edges:
if isinstance(edge.Curve, Part.Circle) and (includePartials or edge.isClosed()):
if isinstance(edge.Curve, Part.Circle) and (
includePartials or edge.isClosed()
):
PathLog.debug("candidate is a circle or ellipse")
if not hasattr(edge.Curve, "Radius"):
PathLog.debug("No radius. Ellipse.")
@@ -142,33 +172,38 @@ def isDrillable(obj, candidate, tooldiameter=None, includePartials=False):
FreeCAD.Console.PrintMessage(
"Found a drillable hole with diameter: {}: "
"too small for the current tool with "
"diameter: {}".format(edge.Curve.Radius * 2, tooldiameter))
"diameter: {}".format(
edge.Curve.Radius * 2, tooldiameter
)
)
else:
drillable = True
PathLog.debug("candidate is drillable: {}".format(drillable))
except Exception as ex: # pylint: disable=broad-except
PathLog.warning(translate("PathUtils", "Issue determine drillability: {}").format(ex))
PathLog.warning(
translate("Path", "Issue determine drillability: {}").format(ex)
)
return drillable
# set at 4 decimal places for testing
def fmt(val):
return format(val, '.4f')
return format(val, ".4f")
def segments(poly):
''' A sequence of (x,y) numeric coordinates pairs '''
"""A sequence of (x,y) numeric coordinates pairs"""
return zip(poly, poly[1:] + [poly[0]])
def loopdetect(obj, edge1, edge2):
'''
"""
Returns a loop wire that includes the two edges.
Useful for detecting boundaries of negative space features ie 'holes'
If a unique loop is not found, returns None
edge1 = edge
edge2 = edge
'''
"""
PathLog.track()
candidates = []
@@ -178,7 +213,9 @@ def loopdetect(obj, edge1, edge2):
candidates.append((wire.hashCode(), wire))
if e.hashCode() == edge2.hashCode():
candidates.append((wire.hashCode(), wire))
loop = set([x for x in candidates if candidates.count(x) > 1]) # return the duplicate item
loop = set(
[x for x in candidates if candidates.count(x) > 1]
) # return the duplicate item
if len(loop) != 1:
return None
loopwire = next(x for x in loop)[1]
@@ -186,18 +223,23 @@ def loopdetect(obj, edge1, edge2):
def horizontalEdgeLoop(obj, edge):
'''horizontalEdgeLoop(obj, edge) ... returns a wire in the horizontal plane, if that is the only horizontal wire the given edge is a part of.'''
"""horizontalEdgeLoop(obj, edge) ... returns a wire in the horizontal plane, if that is the only horizontal wire the given edge is a part of."""
h = edge.hashCode()
wires = [w for w in obj.Shape.Wires if any(e.hashCode() == h for e in w.Edges)]
loops = [w for w in wires if all(PathGeom.isHorizontal(e) for e in w.Edges) and PathGeom.isHorizontal(Part.Face(w))]
loops = [
w
for w in wires
if all(PathGeom.isHorizontal(e) for e in w.Edges)
and PathGeom.isHorizontal(Part.Face(w))
]
if len(loops) == 1:
return loops[0]
return None
def horizontalFaceLoop(obj, face, faceList=None):
'''horizontalFaceLoop(obj, face, faceList=None) ... returns a list of face names which form the walls of a vertical hole face is a part of.
All face names listed in faceList must be part of the hole for the solution to be returned.'''
"""horizontalFaceLoop(obj, face, faceList=None) ... returns a list of face names which form the walls of a vertical hole face is a part of.
All face names listed in faceList must be part of the hole for the solution to be returned."""
wires = [horizontalEdgeLoop(obj, e) for e in face.Edges]
# Not sure if sorting by Area is a premature optimization - but it seems
@@ -208,7 +250,11 @@ def horizontalFaceLoop(obj, face, faceList=None):
hashes = [e.hashCode() for e in wire.Edges]
# find all faces that share a an edge with the wire and are vertical
faces = ["Face%d" % (i + 1) for i, f in enumerate(obj.Shape.Faces) if any(e.hashCode() in hashes for e in f.Edges) and PathGeom.isVertical(f)]
faces = [
"Face%d" % (i + 1)
for i, f in enumerate(obj.Shape.Faces)
if any(e.hashCode() in hashes for e in f.Edges) and PathGeom.isVertical(f)
]
if faceList and not all(f in faces for f in faceList):
continue
@@ -231,13 +277,19 @@ def horizontalFaceLoop(obj, face, faceList=None):
# wire is still closed and it still has the same footprint
bb1 = comp.BoundBox
bb2 = w.BoundBox
if w.isClosed() and PathGeom.isRoughly(bb1.XMin, bb2.XMin) and PathGeom.isRoughly(bb1.XMax, bb2.XMax) and PathGeom.isRoughly(bb1.YMin, bb2.YMin) and PathGeom.isRoughly(bb1.YMax, bb2.YMax):
if (
w.isClosed()
and PathGeom.isRoughly(bb1.XMin, bb2.XMin)
and PathGeom.isRoughly(bb1.XMax, bb2.XMax)
and PathGeom.isRoughly(bb1.YMin, bb2.YMin)
and PathGeom.isRoughly(bb1.YMax, bb2.YMax)
):
return faces
return None
def filterArcs(arcEdge):
'''filterArcs(Edge) -used to split arcs that over 180 degrees. Returns list '''
"""filterArcs(Edge) -used to split arcs that over 180 degrees. Returns list"""
PathLog.track()
s = arcEdge
if isinstance(s.Curve, Part.Circle):
@@ -245,7 +297,7 @@ def filterArcs(arcEdge):
angle = abs(s.LastParameter - s.FirstParameter)
# overhalfcircle = False
goodarc = False
if (angle > math.pi):
if angle > math.pi:
pass
# overhalfcircle = True
else:
@@ -253,9 +305,14 @@ def filterArcs(arcEdge):
if not goodarc:
arcstpt = s.valueAt(s.FirstParameter)
arcmid = s.valueAt(
(s.LastParameter - s.FirstParameter) * 0.5 + s.FirstParameter)
arcquad1 = s.valueAt((s.LastParameter - s.FirstParameter) * 0.25 + s.FirstParameter) # future midpt for arc1
arcquad2 = s.valueAt((s.LastParameter - s.FirstParameter) * 0.75 + s.FirstParameter) # future midpt for arc2
(s.LastParameter - s.FirstParameter) * 0.5 + s.FirstParameter
)
arcquad1 = s.valueAt(
(s.LastParameter - s.FirstParameter) * 0.25 + s.FirstParameter
) # future midpt for arc1
arcquad2 = s.valueAt(
(s.LastParameter - s.FirstParameter) * 0.75 + s.FirstParameter
) # future midpt for arc2
arcendpt = s.valueAt(s.LastParameter)
# reconstruct with 2 arcs
arcseg1 = Part.ArcOfCircle(arcstpt, arcquad1, arcmid)
@@ -277,28 +334,28 @@ def makeWorkplane(shape):
Creates a workplane circle at the ZMin level.
"""
PathLog.track()
loc = FreeCAD.Vector(shape.BoundBox.Center.x,
shape.BoundBox.Center.y,
shape.BoundBox.ZMin)
loc = FreeCAD.Vector(
shape.BoundBox.Center.x, shape.BoundBox.Center.y, shape.BoundBox.ZMin
)
c = Part.makeCircle(10, loc)
return c
def getEnvelope(partshape, subshape=None, depthparams=None):
'''
"""
getEnvelope(partshape, stockheight=None)
returns a shape corresponding to the partshape silhouette extruded to height.
if stockheight is given, the returned shape is extruded to that height otherwise the returned shape
is the height of the original shape boundbox
partshape = solid object
stockheight = float - Absolute Z height of the top of material before cutting.
'''
"""
PathLog.track(partshape, subshape, depthparams)
zShift = 0
if subshape is not None:
if isinstance(subshape, Part.Face):
PathLog.debug('processing a face')
PathLog.debug("processing a face")
sec = Part.makeCompound([subshape])
else:
area = Path.Area(Fill=2, Coplanar=0).add(subshape)
@@ -306,7 +363,11 @@ def getEnvelope(partshape, subshape=None, depthparams=None):
PathLog.debug("About to section with params: {}".format(area.getParams()))
sec = area.makeSections(heights=[0.0], project=True)[0].getShape()
PathLog.debug('partshapeZmin: {}, subshapeZMin: {}, zShift: {}'.format(partshape.BoundBox.ZMin, subshape.BoundBox.ZMin, zShift))
PathLog.debug(
"partshapeZmin: {}, subshapeZMin: {}, zShift: {}".format(
partshape.BoundBox.ZMin, subshape.BoundBox.ZMin, zShift
)
)
else:
area = Path.Area(Fill=2, Coplanar=0).add(partshape)
@@ -318,7 +379,11 @@ def getEnvelope(partshape, subshape=None, depthparams=None):
if depthparams is not None:
eLength = depthparams.safe_height - depthparams.final_depth
zShift = depthparams.final_depth - sec.BoundBox.ZMin
PathLog.debug('boundbox zMIN: {} elength: {} zShift {}'.format(partshape.BoundBox.ZMin, eLength, zShift))
PathLog.debug(
"boundbox zMIN: {} elength: {} zShift {}".format(
partshape.BoundBox.ZMin, eLength, zShift
)
)
else:
eLength = partshape.BoundBox.ZLength - sec.BoundBox.ZMin
@@ -335,35 +400,37 @@ def getEnvelope(partshape, subshape=None, depthparams=None):
# Function to extract offset face from shape
def getOffsetArea(fcShape,
offset,
removeHoles=False,
# Default: XY plane
plane=Part.makeCircle(10),
tolerance=1e-4):
'''Make an offset area of a shape, projected onto a plane.
def getOffsetArea(
fcShape,
offset,
removeHoles=False,
# Default: XY plane
plane=Part.makeCircle(10),
tolerance=1e-4,
):
"""Make an offset area of a shape, projected onto a plane.
Positive offsets expand the area, negative offsets shrink it.
Inspired by _buildPathArea() from PathAreaOp.py module. Adjustments made
based on notes by @sliptonic at this webpage:
https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.'''
PathLog.debug('getOffsetArea()')
https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes."""
PathLog.debug("getOffsetArea()")
areaParams = {}
areaParams['Offset'] = offset
areaParams['Fill'] = 1 # 1
areaParams['Outline'] = removeHoles
areaParams['Coplanar'] = 0
areaParams['SectionCount'] = 1 # -1 = full(all per depthparams??) sections
areaParams['Reorient'] = True
areaParams['OpenMode'] = 0
areaParams['MaxArcPoints'] = 400 # 400
areaParams['Project'] = True
areaParams['FitArcs'] = False # Can be buggy & expensive
areaParams['Deflection'] = tolerance
areaParams['Accuracy'] = tolerance
areaParams['Tolerance'] = 1e-5 # Equal point tolerance
areaParams['Simplify'] = True
areaParams['CleanDistance'] = tolerance / 5
areaParams["Offset"] = offset
areaParams["Fill"] = 1 # 1
areaParams["Outline"] = removeHoles
areaParams["Coplanar"] = 0
areaParams["SectionCount"] = 1 # -1 = full(all per depthparams??) sections
areaParams["Reorient"] = True
areaParams["OpenMode"] = 0
areaParams["MaxArcPoints"] = 400 # 400
areaParams["Project"] = True
areaParams["FitArcs"] = False # Can be buggy & expensive
areaParams["Deflection"] = tolerance
areaParams["Accuracy"] = tolerance
areaParams["Tolerance"] = 1e-5 # Equal point tolerance
areaParams["Simplify"] = True
areaParams["CleanDistance"] = tolerance / 5
area = Path.Area() # Create instance of Area() class object
# Set working plane normal to Z=1
@@ -380,11 +447,16 @@ def getOffsetArea(fcShape,
def reverseEdge(e):
if DraftGeomUtils.geomType(e) == "Circle":
arcstpt = e.valueAt(e.FirstParameter)
arcmid = e.valueAt((e.LastParameter - e.FirstParameter) * 0.5 + e.FirstParameter)
arcmid = e.valueAt(
(e.LastParameter - e.FirstParameter) * 0.5 + e.FirstParameter
)
arcendpt = e.valueAt(e.LastParameter)
arcofCirc = Part.ArcOfCircle(arcendpt, arcmid, arcstpt)
newedge = arcofCirc.toShape()
elif DraftGeomUtils.geomType(e) == "LineSegment" or DraftGeomUtils.geomType(e) == "Line":
elif (
DraftGeomUtils.geomType(e) == "LineSegment"
or DraftGeomUtils.geomType(e) == "Line"
):
stpt = e.valueAt(e.FirstParameter)
endpt = e.valueAt(e.LastParameter)
newedge = Part.makeLine(endpt, stpt)
@@ -393,7 +465,7 @@ def reverseEdge(e):
def getToolControllers(obj, proxy=None):
'''returns all the tool controllers'''
"""returns all the tool controllers"""
if proxy is None:
proxy = obj.Proxy
try:
@@ -408,11 +480,11 @@ def getToolControllers(obj, proxy=None):
def findToolController(obj, proxy, name=None):
'''returns a tool controller with a given name.
"""returns a tool controller with a given name.
If no name is specified, returns the first controller.
if no controller is found, returns None'''
if no controller is found, returns None"""
PathLog.track('name: {}'.format(name))
PathLog.track("name: {}".format(name))
c = None
if UserInput:
c = UserInput.selectedToolController()
@@ -438,12 +510,16 @@ def findToolController(obj, proxy, name=None):
def findParentJob(obj):
'''retrieves a parent job object for an operation or other Path object'''
"""retrieves a parent job object for an operation or other Path object"""
PathLog.track()
for i in obj.InList:
if hasattr(i, 'Proxy') and isinstance(i.Proxy, PathJob.ObjectJob):
if hasattr(i, "Proxy") and isinstance(i.Proxy, PathJob.ObjectJob):
return i
if i.TypeId == "Path::FeaturePython" or i.TypeId == "Path::FeatureCompoundPython" or i.TypeId == "App::DocumentObjectGroup":
if (
i.TypeId == "Path::FeaturePython"
or i.TypeId == "Path::FeatureCompoundPython"
or i.TypeId == "App::DocumentObjectGroup"
):
grandParent = findParentJob(i)
if grandParent is not None:
return grandParent
@@ -451,16 +527,16 @@ def findParentJob(obj):
def GetJobs(jobname=None):
'''returns all jobs in the current document. If name is given, returns that job'''
"""returns all jobs in the current document. If name is given, returns that job"""
if jobname:
return [job for job in PathJob.Instances() if job.Name == jobname]
return PathJob.Instances()
def addToJob(obj, jobname=None):
'''adds a path object to a job
"""adds a path object to a job
obj = obj
jobname = None'''
jobname = None"""
PathLog.track(jobname)
job = None
@@ -486,14 +562,14 @@ def addToJob(obj, jobname=None):
def rapid(x=None, y=None, z=None):
""" Returns gcode string to perform a rapid move."""
"""Returns gcode string to perform a rapid move."""
retstr = "G00"
if (x is not None) or (y is not None) or (z is not None):
if (x is not None):
if x is not None:
retstr += " X" + str("%.4f" % x)
if (y is not None):
if y is not None:
retstr += " Y" + str("%.4f" % y)
if (z is not None):
if z is not None:
retstr += " Z" + str("%.4f" % z)
else:
return ""
@@ -501,19 +577,19 @@ def rapid(x=None, y=None, z=None):
def feed(x=None, y=None, z=None, horizFeed=0, vertFeed=0):
""" Return gcode string to perform a linear feed."""
"""Return gcode string to perform a linear feed."""
retstr = "G01 F"
if(x is None) and (y is None):
if (x is None) and (y is None):
retstr += str("%.4f" % horizFeed)
else:
retstr += str("%.4f" % vertFeed)
if (x is not None) or (y is not None) or (z is not None):
if (x is not None):
if x is not None:
retstr += " X" + str("%.4f" % x)
if (y is not None):
if y is not None:
retstr += " Y" + str("%.4f" % y)
if (z is not None):
if z is not None:
retstr += " Z" + str("%.4f" % z)
else:
return ""
@@ -537,7 +613,10 @@ def arc(cx, cy, sx, sy, ex, ey, horizFeed=0, ez=None, ccw=False):
"""
eps = 0.01
if (math.sqrt((cx - sx)**2 + (cy - sy)**2) - math.sqrt((cx - ex)**2 + (cy - ey)**2)) >= eps:
if (
math.sqrt((cx - sx) ** 2 + (cy - sy) ** 2)
- math.sqrt((cx - ex) ** 2 + (cy - ey) ** 2)
) >= eps:
PathLog.error(translate("Path", "Illegal arc: Start and end radii not equal"))
return ""
@@ -578,7 +657,7 @@ def helicalPlunge(plungePos, rampangle, destZ, startZ, toold, plungeR, horizFeed
helixY = plungePos.y
helixCirc = math.pi * toold * plungeR
dzPerRev = math.sin(rampangle / 180. * math.pi) * helixCirc
dzPerRev = math.sin(rampangle / 180.0 * math.pi) * helixCirc
# Go to the start of the helix position
helixCmds += rapid(helixX, helixY)
@@ -589,14 +668,34 @@ def helicalPlunge(plungePos, rampangle, destZ, startZ, toold, plungeR, horizFeed
curZ = max(startZ - dzPerRev, destZ)
done = False
while not done:
done = (curZ == destZ)
done = curZ == destZ
# NOTE: FreeCAD doesn't render this, but at least LinuxCNC considers it valid
# helixCmds += arc(plungePos.x, plungePos.y, helixX, helixY, helixX, helixY, ez = curZ, ccw=True)
# Use two half-helixes; FreeCAD renders that correctly,
# and it fits with the other code breaking up 360-degree arcs
helixCmds += arc(plungePos.x, plungePos.y, helixX, helixY, helixX - toold * plungeR, helixY, horizFeed, ez=(curZ + lastZ) / 2., ccw=True)
helixCmds += arc(plungePos.x, plungePos.y, helixX - toold * plungeR, helixY, helixX, helixY, horizFeed, ez=curZ, ccw=True)
helixCmds += arc(
plungePos.x,
plungePos.y,
helixX,
helixY,
helixX - toold * plungeR,
helixY,
horizFeed,
ez=(curZ + lastZ) / 2.0,
ccw=True,
)
helixCmds += arc(
plungePos.x,
plungePos.y,
helixX - toold * plungeR,
helixY,
helixX,
helixY,
horizFeed,
ez=curZ,
ccw=True,
)
lastZ = curZ
curZ = max(curZ - dzPerRev, destZ)
@@ -619,7 +718,7 @@ def rampPlunge(edge, rampangle, destZ, startZ):
"""
rampCmds = "(START RAMP PLUNGE)\n"
if(edge is None):
if edge is None:
raise Exception("Ramp plunging requires an edge!")
sPoint = edge.Vertexes[0].Point
@@ -630,7 +729,7 @@ def rampPlunge(edge, rampangle, destZ, startZ):
ePoint = edge.Vertexes[-1].Point
rampDist = edge.Length
rampDZ = math.sin(rampangle / 180. * math.pi) * rampDist
rampDZ = math.sin(rampangle / 180.0 * math.pi) * rampDist
rampCmds += rapid(sPoint.x, sPoint.y)
rampCmds += rapid(z=startZ)
@@ -640,7 +739,7 @@ def rampPlunge(edge, rampangle, destZ, startZ):
curZ = max(startZ - rampDZ, destZ)
done = False
while not done:
done = (curZ == destZ)
done = curZ == destZ
# If it's an arc, handle it!
if isinstance(edge.Curve, Part.Circle):
@@ -656,9 +755,9 @@ def rampPlunge(edge, rampangle, destZ, startZ):
def sort_jobs(locations, keys, attractors=None):
""" sort holes by the nearest neighbor method
keys: two-element list of keys for X and Y coordinates. for example ['x','y']
originally written by m0n5t3r for PathHelix
"""sort holes by the nearest neighbor method
keys: two-element list of keys for X and Y coordinates. for example ['x','y']
originally written by m0n5t3r for PathHelix
"""
if attractors is None:
attractors = []
@@ -671,7 +770,7 @@ def sort_jobs(locations, keys, attractors=None):
attractors = attractors or [keys[0]]
def sqdist(a, b):
""" square Euclidean distance """
"""square Euclidean distance"""
d = 0
for k in keys:
d += (a[k] - b[k]) ** 2
@@ -740,7 +839,9 @@ def guessDepths(objshape, subs=None):
elif fbb.ZMax == fbb.ZMin and fbb.ZMax > bb.ZMin: # face/shelf
final = fbb.ZMin
return depth_params(clearance, safe, start, 1.0, 0.0, final, user_depths=None, equalstep=False)
return depth_params(
clearance, safe, start, 1.0, 0.0, final, user_depths=None, equalstep=False
)
def drillTipLength(tool):
@@ -750,28 +851,36 @@ def drillTipLength(tool):
PathLog.error(translate("Path", "Legacy Tools not supported"))
return 0.0
if not hasattr(tool, 'TipAngle'):
if not hasattr(tool, "TipAngle"):
PathLog.error(translate("Path", "Selected tool is not a drill"))
return 0.0
angle = tool.TipAngle
if angle <= 0 or angle >= 180:
PathLog.error(translate("Path", "Invalid Cutting Edge Angle %.2f, must be >0° and <=180°") % angle)
PathLog.error(
translate("Path", "Invalid Cutting Edge Angle %.2f, must be >0° and <=180°")
% angle
)
return 0.0
theta = math.radians(angle)
length = (float(tool.Diameter) / 2) / math.tan(theta / 2)
if length < 0:
PathLog.error(translate("Path", "Cutting Edge Angle (%.2f) results in negative tool tip length") % angle)
PathLog.error(
translate(
"Path", "Cutting Edge Angle (%.2f) results in negative tool tip length"
)
% angle
)
return 0.0
return length
class depth_params(object):
'''calculates the intermediate depth values for various operations given the starting, ending, and stepdown parameters
"""calculates the intermediate depth values for various operations given the starting, ending, and stepdown parameters
(self, clearance_height, safe_height, start_depth, step_down, z_finish_depth, final_depth, [user_depths=None], equalstep=False)
Note: if user_depths are supplied, only user_depths will be used.
@@ -784,10 +893,20 @@ class depth_params(object):
final_depth: Lowest point of the cutting operation
user_depths: List of specified depths
equalstep: Boolean. If True, steps down except Z_finish_depth will be balanced.
'''
"""
def __init__(self, clearance_height, safe_height, start_depth, step_down, z_finish_step, final_depth, user_depths=None, equalstep=False):
'''self, clearance_height, safe_height, start_depth, step_down, z_finish_depth, final_depth, [user_depths=None], equalstep=False'''
def __init__(
self,
clearance_height,
safe_height,
start_depth,
step_down,
z_finish_step,
final_depth,
user_depths=None,
equalstep=False,
):
"""self, clearance_height, safe_height, start_depth, step_down, z_finish_depth, final_depth, [user_depths=None], equalstep=False"""
self.__clearance_height = clearance_height
self.__safe_height = safe_height
@@ -800,7 +919,7 @@ class depth_params(object):
self.index = 0
if self.__z_finish_step > self.__step_down:
raise ValueError('z_finish_step must be less than step_down')
raise ValueError("z_finish_step must be less than step_down")
def __iter__(self):
self.index = 0
@@ -874,8 +993,8 @@ class depth_params(object):
return self.__user_depths
def __get_depths(self, equalstep=False):
'''returns a list of depths to be used in order from first to last.
equalstep=True: all steps down before the finish pass will be equalized.'''
"""returns a list of depths to be used in order from first to last.
equalstep=True: all steps down before the finish pass will be equalized."""
if self.user_depths is not None:
return self.__user_depths
@@ -895,18 +1014,22 @@ class depth_params(object):
return depths
if equalstep:
depths += self.__equal_steps(self.__start_depth, depths[-1], self.__step_down)[1:]
depths += self.__equal_steps(
self.__start_depth, depths[-1], self.__step_down
)[1:]
else:
depths += self.__fixed_steps(self.__start_depth, depths[-1], self.__step_down)[1:]
depths += self.__fixed_steps(
self.__start_depth, depths[-1], self.__step_down
)[1:]
depths.reverse()
return depths
def __equal_steps(self, start, stop, max_size):
'''returns a list of depths beginning with the bottom (included), ending
"""returns a list of depths beginning with the bottom (included), ending
with the top (not included).
all steps are of equal size, which is as big as possible but not bigger
than max_size.'''
than max_size."""
steps_needed = math.ceil((start - stop) / max_size)
depths = list(numpy.linspace(stop, start, steps_needed, endpoint=False))
@@ -914,10 +1037,10 @@ class depth_params(object):
return depths
def __fixed_steps(self, start, stop, size):
'''returns a list of depths beginning with the bottom (included), ending
"""returns a list of depths beginning with the bottom (included), ending
with the top (not included).
all steps are of size 'size' except the one at the bottom which can be
smaller.'''
smaller."""
fullsteps = int((start - stop) / size)
last_step = start - (fullsteps * size)
@@ -968,17 +1091,17 @@ def simplify3dLine(line, tolerance=1e-4):
def RtoIJ(startpoint, command):
'''
"""
This function takes a startpoint and an arc command in radius mode and
returns an arc command in IJ mode. Useful for preprocessor scripts
'''
if 'R' not in command.Parameters:
raise ValueError('No R parameter in command')
if command.Name not in ['G2', 'G02', 'G03', 'G3']:
raise ValueError('Not an arc command')
"""
if "R" not in command.Parameters:
raise ValueError("No R parameter in command")
if command.Name not in ["G2", "G02", "G03", "G3"]:
raise ValueError("Not an arc command")
endpoint = command.Placement.Base
radius = command.Parameters['R']
radius = command.Parameters["R"]
# calculate the IJ
# we take a vector between the start and endpoints
@@ -988,7 +1111,7 @@ def RtoIJ(startpoint, command):
perp = chord.cross(FreeCAD.Vector(0, 0, 1))
# use pythagoras to get the perp length
plength = math.sqrt(radius**2 - (chord.Length / 2)**2)
plength = math.sqrt(radius ** 2 - (chord.Length / 2) ** 2)
perp.normalize()
perp.scale(plength, plength, plength)
@@ -996,9 +1119,9 @@ def RtoIJ(startpoint, command):
relativecenter = chord.scale(0.5, 0.5, 0.5).add(perp)
# build new command
params = { c: command.Parameters[c] for c in 'XYZF' if c in command.Parameters}
params['I'] = relativecenter.x
params['J'] = relativecenter.y
params = {c: command.Parameters[c] for c in "XYZF" if c in command.Parameters}
params["I"] = relativecenter.x
params["J"] = relativecenter.y
newcommand = Path.Command(command.Name)
newcommand.Parameters = params

View File

@@ -1,34 +1,36 @@
# -*- coding: utf-8 -*-
#***************************************************************************
#* Copyright (c) 2014 Yorik van Havre <yorik@uncreated.net> *
#* *
#* This file is part of the FreeCAD CAx development system. *
#* *
#* 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. *
#* *
#* FreeCAD 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 Lesser General Public License for more details. *
#* *
#* You should have received a copy of the GNU Library General Public *
#* License along with FreeCAD; if not, write to the Free Software *
#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
#* USA *
#* *
#***************************************************************************
# ***************************************************************************
# * Copyright (c) 2014 Yorik van Havre <yorik@uncreated.net> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
# * 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. *
# * *
# * FreeCAD 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 Lesser General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with FreeCAD; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
'''
"""
These are a common functions and classes for creating custom post processors.
'''
"""
from PySide import QtCore, QtGui
import FreeCAD
translate = FreeCAD.Qt.translate
FreeCADGui = None
if FreeCAD.GuiUp:
import FreeCADGui
@@ -41,15 +43,16 @@ class GCodeHighlighter(QtGui.QSyntaxHighlighter):
keywordFormat = QtGui.QTextCharFormat()
keywordFormat.setForeground(QtCore.Qt.cyan)
keywordFormat.setFontWeight(QtGui.QFont.Bold)
keywordPatterns = ["\\bG[0-9]+\\b",
"\\bM[0-9]+\\b"]
keywordPatterns = ["\\bG[0-9]+\\b", "\\bM[0-9]+\\b"]
self.highlightingRules = [(QtCore.QRegExp(pattern), keywordFormat) for pattern in keywordPatterns]
self.highlightingRules = [
(QtCore.QRegExp(pattern), keywordFormat) for pattern in keywordPatterns
]
speedFormat = QtGui.QTextCharFormat()
speedFormat.setFontWeight(QtGui.QFont.Bold)
speedFormat.setForeground(QtCore.Qt.green)
self.highlightingRules.append((QtCore.QRegExp("\\bF[0-9\\.]+\\b"),speedFormat))
self.highlightingRules.append((QtCore.QRegExp("\\bF[0-9\\.]+\\b"), speedFormat))
def highlightBlock(self, text):
for pattern, hlFormat in self.highlightingRules:
@@ -61,12 +64,11 @@ class GCodeHighlighter(QtGui.QSyntaxHighlighter):
index = expression.indexIn(text, index + length)
class GCodeEditorDialog(QtGui.QDialog):
def __init__(self, parent = None):
def __init__(self, parent=None):
if parent is None:
parent = FreeCADGui.getMainWindow()
QtGui.QDialog.__init__(self,parent)
QtGui.QDialog.__init__(self, parent)
layout = QtGui.QVBoxLayout(self)
@@ -83,7 +85,9 @@ class GCodeEditorDialog(QtGui.QDialog):
# OK and Cancel buttons
self.buttons = QtGui.QDialogButtonBox(
QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel,
QtCore.Qt.Horizontal, self)
QtCore.Qt.Horizontal,
self,
)
layout.addWidget(self.buttons)
# restore placement and size
@@ -111,45 +115,68 @@ class GCodeEditorDialog(QtGui.QDialog):
def stringsplit(commandline):
returndict = {'command':None, 'X':None, 'Y':None, 'Z':None, 'A':None, 'B':None, 'F':None, 'T':None, 'S':None, 'I':None, 'J':None,'K':None, 'txt': None}
returndict = {
"command": None,
"X": None,
"Y": None,
"Z": None,
"A": None,
"B": None,
"F": None,
"T": None,
"S": None,
"I": None,
"J": None,
"K": None,
"txt": None,
}
wordlist = [a.strip() for a in commandline.split(" ")]
if wordlist[0][0] == '(':
returndict['command'] = 'message'
returndict['txt'] = wordlist[0]
if wordlist[0][0] == "(":
returndict["command"] = "message"
returndict["txt"] = wordlist[0]
else:
returndict['command'] = wordlist[0]
returndict["command"] = wordlist[0]
for word in wordlist[1:]:
returndict[word[0]] = word[1:]
return returndict
def fmt(num,dec,units):
''' used to format axis moves, feedrate, etc for decimal places and units'''
if units == 'G21': #metric
fnum = '%.*f' % (dec, num)
else: #inch
fnum = '%.*f' % (dec, num/25.4) #since FreeCAD uses metric units internally
def fmt(num, dec, units):
"""used to format axis moves, feedrate, etc for decimal places and units"""
if units == "G21": # metric
fnum = "%.*f" % (dec, num)
else: # inch
fnum = "%.*f" % (dec, num / 25.4) # since FreeCAD uses metric units internally
return fnum
def editor(gcode):
'''pops up a handy little editor to look at the code output '''
"""pops up a handy little editor to look at the code output"""
prefs = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Path")
# default Max Highlighter Size = 512 Ko
defaultMHS = 512 * 1024
mhs = prefs.GetUnsigned('inspecteditorMaxHighlighterSize', defaultMHS)
mhs = prefs.GetUnsigned("inspecteditorMaxHighlighterSize", defaultMHS)
dia = GCodeEditorDialog()
dia.editor.setText(gcode)
gcodeSize = len(dia.editor.toPlainText())
if (gcodeSize <= mhs):
if gcodeSize <= mhs:
# because of poor performance, syntax highlighting is
# limited to mhs octets (default 512 KB).
# It seems than the response time curve has an inflexion near 500 KB
# beyond 500 KB, the response time increases exponentially.
dia.highlighter = GCodeHighlighter(dia.editor.document())
else:
FreeCAD.Console.PrintMessage(translate("Path", "GCode size too big ({} o), disabling syntax highlighter.".format(gcodeSize)))
FreeCAD.Console.PrintMessage(
translate(
"Path",
"GCode size too big ({} o), disabling syntax highlighter.".format(
gcodeSize
),
)
)
result = dia.exec_()
if result: # If user selected 'OK' get modified G Code
final = dia.editor.toPlainText()
@@ -157,14 +184,12 @@ def editor(gcode):
final = gcode
return final
def fcoms(string,commentsym):
''' filter and rebuild comments with user preferred comment symbol'''
if len(commentsym)==1:
s1 = string.replace('(', commentsym)
comment = s1.replace(')', '')
def fcoms(string, commentsym):
"""filter and rebuild comments with user preferred comment symbol"""
if len(commentsym) == 1:
s1 = string.replace("(", commentsym)
comment = s1.replace(")", "")
else:
return string
return comment

View File

@@ -22,7 +22,7 @@
# ***************************************************************************/
'''
"""
This is an example preprocessor file for the Path workbench. Its aim is to
open a gcode file, parse its contents, and create the appropriate objects
in FreeCAD.
@@ -40,7 +40,7 @@ assumed. The user should carefully examine the resulting gcode!
Read the Path Workbench documentation to know how to create Path objects
from GCode.
'''
"""
import os
import FreeCAD
@@ -50,23 +50,23 @@ import re
import PathScripts.PathCustom as PathCustom
import PathScripts.PathCustomGui as PathCustomGui
import PathScripts.PathOpGui as PathOpGui
from PySide import QtCore
from PySide.QtCore import QT_TRANSLATE_NOOP
# LEVEL = PathLog.Level.DEBUG
LEVEL = PathLog.Level.INFO
PathLog.setLevel(LEVEL, PathLog.thisModule())
if LEVEL == PathLog.Level.DEBUG:
if False:
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
PathLog.trackModule(PathLog.thisModule())
else:
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
# to distinguish python built-in open function from the one declared below
if open.__module__ in ['__builtin__', 'io']:
if open.__module__ in ["__builtin__", "io"]:
pythonopen = open
def open(filename):
"called when freecad opens a file."
"""called when freecad opens a file."""
PathLog.track(filename)
docname = os.path.splitext(os.path.basename(filename))[0]
doc = FreeCAD.newDocument(docname)
@@ -83,17 +83,17 @@ def matchToolController(op, toolnumber):
def insert(filename, docname):
"called when freecad imports a file"
"""called when freecad imports a file"""
PathLog.track(filename)
gfile = pythonopen(filename)
gcode = gfile.read()
gfile.close()
# Regular expression to match tool changes in the format 'M6 Tn'
p = re.compile('[mM]+?\s?0?6\s?T\d*\s')
p = re.compile("[mM]+?\s?0?6\s?T\d*\s")
# split the gcode on tool changes
paths = re.split('([mM]+?\s?0?6\s?T\d*\s)', gcode)
paths = re.split("([mM]+?\s?0?6\s?T\d*\s)", gcode)
# iterate the gcode sections and add customs for each
toolnumber = 0
@@ -103,7 +103,7 @@ def insert(filename, docname):
# if the section is a tool change, extract the tool number
m = p.match(path)
if m:
toolnumber = int(m.group().split('T')[-1])
toolnumber = int(m.group().split("T")[-1])
continue
# Parse the gcode and throw away any empty lists
@@ -113,10 +113,15 @@ def insert(filename, docname):
# Create a custom and viewobject
obj = PathCustom.Create("Custom")
res = PathOpGui.CommandResources('Custom', PathCustom.Create,
PathCustomGui.TaskPanelOpPage,
'Path_Custom',
QtCore.QT_TRANSLATE_NOOP('Path_Custom', 'Custom'), '', '')
res = PathOpGui.CommandResources(
"Custom",
PathCustom.Create,
PathCustomGui.TaskPanelOpPage,
"Path_Custom",
QT_TRANSLATE_NOOP("Path_Custom", "Custom"),
"",
"",
)
obj.ViewObject.Proxy = PathOpGui.ViewProvider(obj.ViewObject, res)
obj.ViewObject.Proxy.setDeleteObjectsOnReject(False)
@@ -127,20 +132,28 @@ def insert(filename, docname):
FreeCAD.ActiveDocument.recompute()
def parse(inputstring):
"parse(inputstring): returns a parsed output string"
supported = ['G0', 'G00',
'G1', 'G01',
'G2', 'G02',
'G3', 'G03',
'G81', 'G82', 'G83',
'G90', 'G91']
supported = [
"G0",
"G00",
"G1",
"G01",
"G2",
"G02",
"G3",
"G03",
"G81",
"G82",
"G83",
"G90",
"G91",
]
axis = ["X", "Y", "Z", "A", "B", "C", "U", "V", "W"]
print("preprocessing...")
FreeCAD.Console.PrintMessage("preprocessing...\n")
PathLog.track(inputstring)
# split the input by line
lines = inputstring.splitlines()
@@ -178,7 +191,7 @@ def parse(inputstring):
elif currcommand[0] in axis and lastcommand:
output.append(lastcommand + " " + lin)
print("done preprocessing.")
FreeCAD.Console.PrintMessage("done preprocessing.\n")
return output

File diff suppressed because it is too large Load Diff

View File

@@ -35,7 +35,7 @@ from FreeCAD import Units
import PathScripts.PathUtil as PathUtil
import PathScripts.PostUtils as PostUtils
Revised = '2020-11-03' # Revision date for this file.
Revised = "2020-11-03" # Revision date for this file.
# *****************************************************************************
# * Due to the fundamentals of the FreeCAD pre-processor, *
@@ -47,155 +47,148 @@ Revised = '2020-11-03' # Revision date for this file.
# *****************************************************************************
TOOLTIP = '''
TOOLTIP = """
Generate g-code from a Path that is compatible with the Marlin controller.
import marlin_post
marlin_post.export(object, "/path/to/file.nc")
'''
"""
# *****************************************************************************
# * Initial configuration, not changeable *
# *****************************************************************************
MOTION_MODE = 'G90' # G90 only, for absolute moves
WORK_PLANE = 'G17' # G17 only, XY plane, for vertical milling
UNITS = 'G21' # G21 only, for metric
UNIT_FORMAT = 'mm'
UNIT_FEED_FORMAT = 'mm/s'
MOTION_MODE = "G90" # G90 only, for absolute moves
WORK_PLANE = "G17" # G17 only, XY plane, for vertical milling
UNITS = "G21" # G21 only, for metric
UNIT_FORMAT = "mm"
UNIT_FEED_FORMAT = "mm/s"
# *****************************************************************************
# * Initial configuration, changeable via command line arguments *
# *****************************************************************************
PRECISION = 3 # Decimal places displayed for metric
DRILL_RETRACT_MODE = 'G98' # End of drill-cycle retractation type. G99
PRECISION = 3 # Decimal places displayed for metric
DRILL_RETRACT_MODE = "G98" # End of drill-cycle retractation type. G99
# is the alternative.
TRANSLATE_DRILL_CYCLES = True # If true, G81, G82, and G83 are translated
TRANSLATE_DRILL_CYCLES = True # If true, G81, G82, and G83 are translated
# into G0/G1 moves
OUTPUT_TOOL_CHANGE = False # Do not output M6 tool change (comment it)
RETURN_TO = None # None = No movement at end of program
SPINDLE_WAIT = 3 # 0 == No waiting after M3 / M4
MODAL = False # True: Commands are suppressed if they are
OUTPUT_TOOL_CHANGE = False # Do not output M6 tool change (comment it)
RETURN_TO = None # None = No movement at end of program
SPINDLE_WAIT = 3 # 0 == No waiting after M3 / M4
MODAL = False # True: Commands are suppressed if they are
# the same as the previous line
LINENR = 100 # Line number starting value
LINEINCR = 10 # Line number increment
PRE_OPERATION = '''''' # Pre operation text will be inserted before
LINENR = 100 # Line number starting value
LINEINCR = 10 # Line number increment
PRE_OPERATION = """""" # Pre operation text will be inserted before
# every operation
POST_OPERATION = '''''' # Post operation text will be inserted after
POST_OPERATION = """""" # Post operation text will be inserted after
# every operation
TOOL_CHANGE = '''''' # Tool Change commands will be inserted
TOOL_CHANGE = """""" # Tool Change commands will be inserted
# before a tool change
# *****************************************************************************
# * Initial gcode output options, changeable via command line arguments *
# *****************************************************************************
OUTPUT_HEADER = True # Output header in output gcode file
OUTPUT_COMMENTS = True # Comments in output gcode file
OUTPUT_FINISH = False # Include an operation finished comment
OUTPUT_PATH = False # Include a Path: comment
OUTPUT_MARLIN_CONFIG = False # Display expected #defines for Marlin config
OUTPUT_LINE_NUMBERS = False # Output line numbers in output gcode file
OUTPUT_BCNC = False # Add bCNC operation block headers in output
OUTPUT_HEADER = True # Output header in output gcode file
OUTPUT_COMMENTS = True # Comments in output gcode file
OUTPUT_FINISH = False # Include an operation finished comment
OUTPUT_PATH = False # Include a Path: comment
OUTPUT_MARLIN_CONFIG = False # Display expected #defines for Marlin config
OUTPUT_LINE_NUMBERS = False # Output line numbers in output gcode file
OUTPUT_BCNC = False # Add bCNC operation block headers in output
# gcode file
SHOW_EDITOR = True # Display the resulting gcode file
SHOW_EDITOR = True # Display the resulting gcode file
# *****************************************************************************
# * Command line arguments *
# *****************************************************************************
parser = argparse.ArgumentParser(prog='marlin', add_help=False)
parser = argparse.ArgumentParser(prog="marlin", add_help=False)
parser.add_argument("--header", action="store_true", help="output headers (default)")
parser.add_argument("--no-header", action="store_true", help="suppress header output")
parser.add_argument("--comments", action="store_true", help="output comment (default)")
parser.add_argument(
'--header',
action='store_true',
help='output headers (default)')
"--no-comments", action="store_true", help="suppress comment output"
)
parser.add_argument(
'--no-header',
action='store_true',
help='suppress header output')
"--finish-comments", action="store_true", help="output finish-comment"
)
parser.add_argument(
'--comments',
action='store_true',
help='output comment (default)')
"--no-finish-comments",
action="store_true",
help="suppress finish-comment output (default)",
)
parser.add_argument("--path-comments", action="store_true", help="output path-comment")
parser.add_argument(
'--no-comments',
action='store_true',
help='suppress comment output')
"--no-path-comments",
action="store_true",
help="suppress path-comment output (default)",
)
parser.add_argument(
'--finish-comments',
action='store_true',
help='output finish-comment')
"--marlin-config", action="store_true", help="output #defines for Marlin"
)
parser.add_argument(
'--no-finish-comments',
action='store_true',
help='suppress finish-comment output (default)')
"--no-marlin-config",
action="store_true",
help="suppress output #defines for Marlin (default)",
)
parser.add_argument(
'--path-comments',
action='store_true',
help='output path-comment')
"--line-numbers", action="store_true", help="prefix with line numbers"
)
parser.add_argument(
'--no-path-comments',
action='store_true',
help='suppress path-comment output (default)')
"--no-line-numbers",
action="store_true",
help="do not prefix with line numbers (default)",
)
parser.add_argument(
'--marlin-config',
action='store_true',
help='output #defines for Marlin')
"--show-editor",
action="store_true",
help="pop up editor before writing output (default)",
)
parser.add_argument(
'--no-marlin-config',
action='store_true',
help='suppress output #defines for Marlin (default)')
"--no-show-editor",
action="store_true",
help="do not pop up editor before writing output",
)
parser.add_argument(
'--line-numbers',
action='store_true',
help='prefix with line numbers')
"--precision", default="3", help="number of digits of precision, default=3"
)
parser.add_argument(
'--no-line-numbers',
action='store_true',
help='do not prefix with line numbers (default)')
"--translate_drill",
action="store_true",
help="translate drill cycles G81, G82, G83 into G0/G1 movements (default)",
)
parser.add_argument(
'--show-editor',
action='store_true',
help='pop up editor before writing output (default)')
"--no-translate_drill",
action="store_true",
help="do not translate drill cycles G81, G82, G83 into G0/G1 movements",
)
parser.add_argument(
'--no-show-editor',
action='store_true',
help='do not pop up editor before writing output')
"--preamble", help='set commands to be issued before the first command, default=""'
)
parser.add_argument(
'--precision',
default='3',
help='number of digits of precision, default=3')
"--postamble", help='set commands to be issued after the last command, default="M5"'
)
parser.add_argument(
'--translate_drill',
action='store_true',
help='translate drill cycles G81, G82, G83 into G0/G1 movements (default)')
"--tool-change", action="store_true", help="Insert M6 for all tool changes"
)
parser.add_argument(
'--no-translate_drill',
action='store_true',
help='do not translate drill cycles G81, G82, G83 into G0/G1 movements')
parser.add_argument(
'--preamble',
help='set commands to be issued before the first command, default=""')
parser.add_argument(
'--postamble',
help='set commands to be issued after the last command, default="M5"')
parser.add_argument(
'--tool-change', action='store_true',
help='Insert M6 for all tool changes')
parser.add_argument(
'--wait-for-spindle',
"--wait-for-spindle",
type=int,
default=3,
help='Wait for spindle to reach desired speed after M3 or M4, default=0')
help="Wait for spindle to reach desired speed after M3 or M4, default=0",
)
parser.add_argument(
'--return-to',
default='',
help='When done, move to, e.g. --return-to="3.175, 4.702, 50.915"')
"--return-to",
default="",
help='When done, move to, e.g. --return-to="3.175, 4.702, 50.915"',
)
parser.add_argument(
'--bcnc',
action='store_true',
help='Add Job operations as bCNC block headers. \
Consider suppressing existing comments: Add argument --no-comments')
"--bcnc",
action="store_true",
help="Add Job operations as bCNC block headers. \
Consider suppressing existing comments: Add argument --no-comments",
)
parser.add_argument(
'--no-bcnc',
action='store_true',
help='suppress bCNC block header output (default)')
"--no-bcnc", action="store_true", help="suppress bCNC block header output (default)"
)
TOOLTIP_ARGS = parser.format_help()
# *****************************************************************************
@@ -209,19 +202,19 @@ TOOLTIP_ARGS = parser.format_help()
# *****************************************************************************
# Default preamble text will appear at the beginning of the gcode output file.
PREAMBLE = ''''''
PREAMBLE = """"""
# Default postamble text will appear following the last operation.
POSTAMBLE = '''M5
'''
POSTAMBLE = """M5
"""
# *****************************************************************************
# * Internal global variables *
# *****************************************************************************
MOTION_COMMANDS = ['G0', 'G00', 'G1', 'G01', 'G2', 'G02', 'G3', 'G03']
RAPID_MOVES = ['G0', 'G00'] # Rapid moves gcode commands definition
SUPPRESS_COMMANDS = [''] # These commands are ignored by commenting them out
COMMAND_SPACE = ' '
MOTION_COMMANDS = ["G0", "G00", "G1", "G01", "G2", "G02", "G3", "G03"]
RAPID_MOVES = ["G0", "G00"] # Rapid moves gcode commands definition
SUPPRESS_COMMANDS = [""] # These commands are ignored by commenting them out
COMMAND_SPACE = " "
# Global variables storing current position (Use None for safety.)
CURRENT_X = None
CURRENT_Y = None
@@ -290,9 +283,9 @@ def processArguments(argstring):
OUTPUT_TOOL_CHANGE = True
if args.return_to:
RETURN_TO = args.return_to
if RETURN_TO.find(',') == -1:
if RETURN_TO.find(",") == -1:
RETURN_TO = None
print('--return-to coordinates must be specified as:')
print("--return-to coordinates must be specified as:")
print('--return-to "x.n,y.n,z.n"')
if args.bcnc:
OUTPUT_BCNC = True
@@ -311,14 +304,14 @@ def processArguments(argstring):
def dump(obj):
for attr in dir(obj):
try:
if attr.startswith('__'):
if attr.startswith("__"):
continue
print('>' + attr + '<')
print(">" + attr + "<")
attr_text = "%s = %s" % (attr, getattr(obj, attr))
if attr in ['HorizFeed', 'VertFeed']:
print('==============\n', attr_text)
if 'mm/s' in attr_text:
print('===> metric values <===')
if attr in ["HorizFeed", "VertFeed"]:
print("==============\n", attr_text)
if "mm/s" in attr_text:
print("===> metric values <===")
except Exception: # Insignificant errors
# print('==>', obj, attr)
pass
@@ -334,128 +327,134 @@ def export(objectslist, filename, argstring):
global MOTION_MODE
global SUPPRESS_COMMANDS
print('Post Processor: ' + __name__ + ' postprocessing...')
gcode = ''
print("Post Processor: " + __name__ + " postprocessing...")
gcode = ""
# Write header:
if OUTPUT_HEADER:
gcode += linenumber() + '(Exported by FreeCAD)\n'
gcode += linenumber() + '(Post Processor: ' + __name__
gcode += '.py, version: ' + Revised + ')\n'
gcode += linenumber() + '(Output Time:' + str(datetime.now()) + ')\n'
gcode += linenumber() + "(Exported by FreeCAD)\n"
gcode += linenumber() + "(Post Processor: " + __name__
gcode += ".py, version: " + Revised + ")\n"
gcode += linenumber() + "(Output Time:" + str(datetime.now()) + ")\n"
# Suppress drill-cycle commands:
if TRANSLATE_DRILL_CYCLES:
SUPPRESS_COMMANDS += ['G80', 'G98', 'G99']
SUPPRESS_COMMANDS += ["G80", "G98", "G99"]
# Write the preamble:
if OUTPUT_COMMENTS:
gcode += linenumber() + '(Begin preamble)\n'
gcode += linenumber() + "(Begin preamble)\n"
for line in PREAMBLE.splitlines(True):
gcode += linenumber() + line
# Write these settings AFTER the preamble,
# to prevent the preamble from changing these:
if OUTPUT_COMMENTS:
gcode += linenumber() + '(Default Configuration)\n'
gcode += linenumber() + MOTION_MODE + '\n'
gcode += linenumber() + UNITS + '\n'
gcode += linenumber() + WORK_PLANE + '\n'
gcode += linenumber() + "(Default Configuration)\n"
gcode += linenumber() + MOTION_MODE + "\n"
gcode += linenumber() + UNITS + "\n"
gcode += linenumber() + WORK_PLANE + "\n"
for obj in objectslist:
# Debug...
# print('\n' + '*'*70 + '\n')
# dump(obj)
# print('\n' + '*'*70 + '\n')
if not hasattr(obj, 'Path'):
print('The object ' + obj.Name +
' is not a path. Please select only path and Compounds.')
if not hasattr(obj, "Path"):
print(
"The object "
+ obj.Name
+ " is not a path. Please select only path and Compounds."
)
return
# Skip inactive operations:
if PathUtil.opProperty(obj, 'Active') is False:
if PathUtil.opProperty(obj, "Active") is False:
continue
# Do the pre_op:
if OUTPUT_BCNC:
gcode += linenumber() + '(Block-name: ' + obj.Label + ')\n'
gcode += linenumber() + '(Block-expand: 0)\n'
gcode += linenumber() + '(Block-enable: 1)\n'
gcode += linenumber() + "(Block-name: " + obj.Label + ")\n"
gcode += linenumber() + "(Block-expand: 0)\n"
gcode += linenumber() + "(Block-enable: 1)\n"
if OUTPUT_COMMENTS:
gcode += linenumber() + '(Begin operation: ' + obj.Label + ')\n'
gcode += linenumber() + "(Begin operation: " + obj.Label + ")\n"
for line in PRE_OPERATION.splitlines(True):
gcode += linenumber() + line
# Get coolant mode:
coolantMode = 'None' # None is the word returned from the operation
if hasattr(obj, 'CoolantMode') or hasattr(obj, 'Base') and \
hasattr(obj.Base, 'CoolantMode'):
if hasattr(obj, 'CoolantMode'):
coolantMode = "None" # None is the word returned from the operation
if (
hasattr(obj, "CoolantMode")
or hasattr(obj, "Base")
and hasattr(obj.Base, "CoolantMode")
):
if hasattr(obj, "CoolantMode"):
coolantMode = obj.CoolantMode
else:
coolantMode = obj.Base.CoolantMode
# Turn coolant on if required:
if OUTPUT_COMMENTS:
if not coolantMode == 'None':
gcode += linenumber() + '(Coolant On:' + coolantMode + ')\n'
if coolantMode == 'Flood':
gcode += linenumber() + 'M8\n'
if coolantMode == 'Mist':
gcode += linenumber() + 'M7\n'
if not coolantMode == "None":
gcode += linenumber() + "(Coolant On:" + coolantMode + ")\n"
if coolantMode == "Flood":
gcode += linenumber() + "M8\n"
if coolantMode == "Mist":
gcode += linenumber() + "M7\n"
# Parse the op:
gcode += parse(obj)
# Do the post_op:
if OUTPUT_COMMENTS and OUTPUT_FINISH:
gcode += linenumber() + '(Finish operation: ' + obj.Label + ')\n'
gcode += linenumber() + "(Finish operation: " + obj.Label + ")\n"
for line in POST_OPERATION.splitlines(True):
gcode += linenumber() + line
# Turn coolant off if previously enabled:
if not coolantMode == 'None':
if not coolantMode == "None":
if OUTPUT_COMMENTS:
gcode += linenumber() + '(Coolant Off:' + coolantMode + ')\n'
gcode += linenumber() + 'M9\n'
gcode += linenumber() + "(Coolant Off:" + coolantMode + ")\n"
gcode += linenumber() + "M9\n"
# Do the post_amble:
if OUTPUT_BCNC:
gcode += linenumber() + '(Block-name: post_amble)\n'
gcode += linenumber() + '(Block-expand: 0)\n'
gcode += linenumber() + '(Block-enable: 1)\n'
gcode += linenumber() + "(Block-name: post_amble)\n"
gcode += linenumber() + "(Block-expand: 0)\n"
gcode += linenumber() + "(Block-enable: 1)\n"
if OUTPUT_COMMENTS:
gcode += linenumber() + '(Begin postamble)\n'
gcode += linenumber() + "(Begin postamble)\n"
for line in POSTAMBLE.splitlines(True):
gcode += linenumber() + line
# Optionally add a final XYZ position to the end of the gcode:
if RETURN_TO:
first_comma = RETURN_TO.find(',')
last_comma = RETURN_TO.rfind(',') # == first_comma if only one comma
ref_X = ' X' + RETURN_TO[0: first_comma].strip()
first_comma = RETURN_TO.find(",")
last_comma = RETURN_TO.rfind(",") # == first_comma if only one comma
ref_X = " X" + RETURN_TO[0:first_comma].strip()
# Z is optional:
if last_comma != first_comma:
ref_Z = ' Z' + RETURN_TO[last_comma + 1:].strip()
ref_Y = ' Y' + RETURN_TO[first_comma + 1:last_comma].strip()
ref_Z = " Z" + RETURN_TO[last_comma + 1 :].strip()
ref_Y = " Y" + RETURN_TO[first_comma + 1 : last_comma].strip()
else:
ref_Z = ''
ref_Y = ' Y' + RETURN_TO[first_comma + 1:].strip()
ref_Z = ""
ref_Y = " Y" + RETURN_TO[first_comma + 1 :].strip()
gcode += linenumber() + 'G0' + ref_X + ref_Y + ref_Z + '\n'
gcode += linenumber() + "G0" + ref_X + ref_Y + ref_Z + "\n"
# Optionally add recommended Marlin 2.x configuration to gcode file:
if OUTPUT_MARLIN_CONFIG:
gcode += linenumber() + '(Marlin 2.x Configuration)\n'
gcode += linenumber() + '(The following should be enabled in)\n'
gcode += linenumber() + '(the configuration files of Marlin 2.x)\n'
gcode += linenumber() + '(#define ARC_SUPPORT)\n'
gcode += linenumber() + '(#define CNC_COORDINATE_SYSTEMS)\n'
gcode += linenumber() + '(#define PAREN_COMMENTS)\n'
gcode += linenumber() + '(#define GCODE_MOTION_MODES)\n'
gcode += linenumber() + '(#define G0_FEEDRATE)\n'
gcode += linenumber() + '(define VARIABLE_G0_FEEDRATE)\n'
gcode += linenumber() + "(Marlin 2.x Configuration)\n"
gcode += linenumber() + "(The following should be enabled in)\n"
gcode += linenumber() + "(the configuration files of Marlin 2.x)\n"
gcode += linenumber() + "(#define ARC_SUPPORT)\n"
gcode += linenumber() + "(#define CNC_COORDINATE_SYSTEMS)\n"
gcode += linenumber() + "(#define PAREN_COMMENTS)\n"
gcode += linenumber() + "(#define GCODE_MOTION_MODES)\n"
gcode += linenumber() + "(#define G0_FEEDRATE)\n"
gcode += linenumber() + "(define VARIABLE_G0_FEEDRATE)\n"
# Show the gcode result dialog:
if FreeCAD.GuiUp and SHOW_EDITOR:
@@ -469,26 +468,26 @@ def export(objectslist, filename, argstring):
else:
final = gcode
print('Done postprocessing.')
print("Done postprocessing.")
# Write the file:
with open(filename, 'w') as fp:
with open(filename, "w") as fp:
fp.write(final)
def linenumber():
if not OUTPUT_LINE_NUMBERS:
return ''
return ""
global LINENR
global LINEINCR
LINENR += LINEINCR
return 'N' + str(LINENR) + ' '
return "N" + str(LINENR) + " "
def format_outlist(strTable):
# construct the line for the final output
global COMMAND_SPACE
s = ''
s = ""
for w in strTable:
s += w + COMMAND_SPACE
return s.strip()
@@ -501,27 +500,46 @@ def parse(pathobj):
global CURRENT_Y
global CURRENT_Z
out = ''
out = ""
lastcommand = None
precision_string = '.' + str(PRECISION) + 'f'
precision_string = "." + str(PRECISION) + "f"
params = ['X', 'Y', 'Z', 'A', 'B', 'C', 'U', 'V', 'W', 'I', 'J', 'K', 'F',
'S', 'T', 'Q', 'R', 'L', 'P']
params = [
"X",
"Y",
"Z",
"A",
"B",
"C",
"U",
"V",
"W",
"I",
"J",
"K",
"F",
"S",
"T",
"Q",
"R",
"L",
"P",
]
if hasattr(pathobj, 'Group'): # We have a compound or project.
if hasattr(pathobj, "Group"): # We have a compound or project.
if OUTPUT_COMMENTS:
out += linenumber() + '(Compound: ' + pathobj.Label + ')\n'
out += linenumber() + "(Compound: " + pathobj.Label + ")\n"
for p in pathobj.Group:
out += parse(p)
return out
else: # Parsing simple path
# groups might contain non-path things like stock.
if not hasattr(pathobj, 'Path'):
if not hasattr(pathobj, "Path"):
return out
if OUTPUT_COMMENTS and OUTPUT_PATH:
out += linenumber() + '(Path: ' + pathobj.Label + ')\n'
out += linenumber() + "(Path: " + pathobj.Label + ")\n"
for c in pathobj.Path.Commands:
outlist = []
@@ -538,96 +556,102 @@ def parse(pathobj):
# Add the remaining parameters in order:
for param in params:
if param in c.Parameters:
if param == 'F':
if param == "F":
if command not in RAPID_MOVES:
feedRate = Units.Quantity(
c.Parameters['F'], FreeCAD.Units.Velocity)
c.Parameters["F"], FreeCAD.Units.Velocity
)
if feedRate.getValueAs(UNIT_FEED_FORMAT) > 0.0:
outlist.append(param + format(float(
feedRate.getValueAs(UNIT_FEED_FORMAT)),
precision_string))
elif param in ['T', 'H', 'D', 'S', 'P', 'L']:
outlist.append(
param
+ format(
float(feedRate.getValueAs(UNIT_FEED_FORMAT)),
precision_string,
)
)
elif param in ["T", "H", "D", "S", "P", "L"]:
outlist.append(param + str(c.Parameters[param]))
elif param in ['A', 'B', 'C']:
outlist.append(param + format(
c.Parameters[param], precision_string))
elif param in ["A", "B", "C"]:
outlist.append(
param + format(c.Parameters[param], precision_string)
)
# [X, Y, Z, U, V, W, I, J, K, R, Q]
else:
pos = Units.Quantity(
c.Parameters[param], FreeCAD.Units.Length)
outlist.append(param + format(float(
pos.getValueAs(UNIT_FORMAT)), precision_string))
pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length)
outlist.append(
param
+ format(
float(pos.getValueAs(UNIT_FORMAT)), precision_string
)
)
# Store the latest command:
lastcommand = command
# Capture the current position for subsequent calculations:
if command in MOTION_COMMANDS:
if 'X' in c.Parameters:
CURRENT_X = Units.Quantity(
c.Parameters['X'], FreeCAD.Units.Length)
if 'Y' in c.Parameters:
CURRENT_Y = Units.Quantity(
c.Parameters['Y'], FreeCAD.Units.Length)
if 'Z' in c.Parameters:
CURRENT_Z = Units.Quantity(
c.Parameters['Z'], FreeCAD.Units.Length)
if "X" in c.Parameters:
CURRENT_X = Units.Quantity(c.Parameters["X"], FreeCAD.Units.Length)
if "Y" in c.Parameters:
CURRENT_Y = Units.Quantity(c.Parameters["Y"], FreeCAD.Units.Length)
if "Z" in c.Parameters:
CURRENT_Z = Units.Quantity(c.Parameters["Z"], FreeCAD.Units.Length)
if command in ('G98', 'G99'):
if command in ("G98", "G99"):
DRILL_RETRACT_MODE = command
if TRANSLATE_DRILL_CYCLES:
if command in ('G81', 'G82', 'G83'):
if command in ("G81", "G82", "G83"):
out += drill_translate(outlist, command, c.Parameters)
# Erase the line just translated:
outlist = []
if SPINDLE_WAIT > 0:
if command in ('M3', 'M03', 'M4', 'M04'):
out += linenumber() + format_outlist(outlist) + '\n'
if command in ("M3", "M03", "M4", "M04"):
out += linenumber() + format_outlist(outlist) + "\n"
# Marlin: P for milliseconds, S for seconds, change P to S
out += linenumber()
out += format_outlist(['G4', 'S%s' % SPINDLE_WAIT])
out += '\n'
out += format_outlist(["G4", "S%s" % SPINDLE_WAIT])
out += "\n"
outlist = []
# Check for Tool Change:
if command in ('M6', 'M06'):
if command in ("M6", "M06"):
if OUTPUT_COMMENTS:
out += linenumber() + '(Begin toolchange)\n'
out += linenumber() + "(Begin toolchange)\n"
if OUTPUT_TOOL_CHANGE:
for line in TOOL_CHANGE.splitlines(True):
out += linenumber() + line
if not OUTPUT_TOOL_CHANGE and OUTPUT_COMMENTS:
outlist[0] = '(' + outlist[0]
outlist[-1] = outlist[-1] + ')'
outlist[0] = "(" + outlist[0]
outlist[-1] = outlist[-1] + ")"
if not OUTPUT_TOOL_CHANGE and not OUTPUT_COMMENTS:
outlist = []
if command == 'message':
if command == "message":
if OUTPUT_COMMENTS:
outlist.pop(0) # remove the command
else:
out = []
if command in SUPPRESS_COMMANDS:
outlist[0] = '(' + outlist[0]
outlist[-1] = outlist[-1] + ')'
outlist[0] = "(" + outlist[0]
outlist[-1] = outlist[-1] + ")"
# Remove embedded comments:
if not OUTPUT_COMMENTS:
tmplist = []
list_index = 0
while list_index < len(outlist):
left_index = outlist[list_index].find('(')
left_index = outlist[list_index].find("(")
if left_index == -1: # Not a comment
tmplist.append(outlist[list_index])
else: # This line contains a comment, and possibly more
right_index = outlist[list_index].find(')')
comment_area = outlist[list_index][
left_index: right_index + 1]
line_minus_comment = outlist[list_index].replace(
comment_area, '').strip()
right_index = outlist[list_index].find(")")
comment_area = outlist[list_index][left_index : right_index + 1]
line_minus_comment = (
outlist[list_index].replace(comment_area, "").strip()
)
if line_minus_comment:
# Line contained more than just a comment
tmplist.append(line_minus_comment)
@@ -637,7 +661,7 @@ def parse(pathobj):
# Prepend a line number and append a newline
if len(outlist) >= 1:
out += linenumber() + format_outlist(outlist) + '\n'
out += linenumber() + format_outlist(outlist) + "\n"
return out
@@ -658,61 +682,56 @@ def drill_translate(outlist, cmd, params):
global UNIT_FEED_FORMAT
class Drill: # Using a class is necessary for the nested functions.
gcode = ''
gcode = ""
strFormat = '.' + str(PRECISION) + 'f'
strFormat = "." + str(PRECISION) + "f"
if OUTPUT_COMMENTS: # Comment the original command
outlist[0] = '(' + outlist[0]
outlist[-1] = outlist[-1] + ')'
Drill.gcode += linenumber() + format_outlist(outlist) + '\n'
outlist[0] = "(" + outlist[0]
outlist[-1] = outlist[-1] + ")"
Drill.gcode += linenumber() + format_outlist(outlist) + "\n"
# Cycle conversion only converts the cycles in the XY plane (G17).
# --> ZX (G18) and YZ (G19) planes produce false gcode.
drill_X = Units.Quantity(params['X'], FreeCAD.Units.Length)
drill_Y = Units.Quantity(params['Y'], FreeCAD.Units.Length)
drill_Z = Units.Quantity(params['Z'], FreeCAD.Units.Length)
drill_R = Units.Quantity(params['R'], FreeCAD.Units.Length)
drill_F = Units.Quantity(params['F'], FreeCAD.Units.Velocity)
if cmd == 'G82':
drill_DwellTime = params['P']
elif cmd == 'G83':
drill_Step = Units.Quantity(params['Q'], FreeCAD.Units.Length)
drill_X = Units.Quantity(params["X"], FreeCAD.Units.Length)
drill_Y = Units.Quantity(params["Y"], FreeCAD.Units.Length)
drill_Z = Units.Quantity(params["Z"], FreeCAD.Units.Length)
drill_R = Units.Quantity(params["R"], FreeCAD.Units.Length)
drill_F = Units.Quantity(params["F"], FreeCAD.Units.Velocity)
if cmd == "G82":
drill_DwellTime = params["P"]
elif cmd == "G83":
drill_Step = Units.Quantity(params["Q"], FreeCAD.Units.Length)
# R less than Z is error
if drill_R < drill_Z:
Drill.gcode += linenumber() + '(drill cycle error: R less than Z )\n'
Drill.gcode += linenumber() + "(drill cycle error: R less than Z )\n"
return Drill.gcode
# Z height to retract to when drill cycle is done:
if DRILL_RETRACT_MODE == 'G98' and CURRENT_Z > drill_R:
if DRILL_RETRACT_MODE == "G98" and CURRENT_Z > drill_R:
RETRACT_Z = CURRENT_Z
else:
RETRACT_Z = drill_R
# Z motion nested functions:
def rapid_Z_to(new_Z):
Drill.gcode += linenumber() + 'G0 Z'
Drill.gcode += format(
float(new_Z.getValueAs(UNIT_FORMAT)), strFormat) + '\n'
Drill.gcode += linenumber() + "G0 Z"
Drill.gcode += format(float(new_Z.getValueAs(UNIT_FORMAT)), strFormat) + "\n"
def feed_Z_to(new_Z):
Drill.gcode += linenumber() + 'G1 Z'
Drill.gcode += format(
float(new_Z.getValueAs(UNIT_FORMAT)), strFormat) + ' F'
Drill.gcode += format(
float(drill_F.getValueAs(UNIT_FEED_FORMAT)), '.2f') + '\n'
Drill.gcode += linenumber() + "G1 Z"
Drill.gcode += format(float(new_Z.getValueAs(UNIT_FORMAT)), strFormat) + " F"
Drill.gcode += format(float(drill_F.getValueAs(UNIT_FEED_FORMAT)), ".2f") + "\n"
# Make sure that Z is not below RETRACT_Z:
if CURRENT_Z < RETRACT_Z:
rapid_Z_to(RETRACT_Z)
# Rapid to hole position XY:
Drill.gcode += linenumber() + 'G0 X'
Drill.gcode += format(
float(drill_X.getValueAs(UNIT_FORMAT)), strFormat) + ' Y'
Drill.gcode += format(
float(drill_Y.getValueAs(UNIT_FORMAT)), strFormat) + '\n'
Drill.gcode += linenumber() + "G0 X"
Drill.gcode += format(float(drill_X.getValueAs(UNIT_FORMAT)), strFormat) + " Y"
Drill.gcode += format(float(drill_Y.getValueAs(UNIT_FORMAT)), strFormat) + "\n"
# Rapid to R:
rapid_Z_to(drill_R)
@@ -728,13 +747,13 @@ def drill_translate(outlist, cmd, params):
# * G99 After the hole has been drilled, retract to R height *
# * Select G99 only if safe to move from hole to hole at the R height *
# *************************************************************************
if cmd in ('G81', 'G82'):
if cmd in ("G81", "G82"):
feed_Z_to(drill_Z) # Drill hole in one step
if cmd == 'G82': # Dwell time delay at the bottom of the hole
Drill.gcode += linenumber() + 'G4 S' + str(drill_DwellTime) + '\n'
if cmd == "G82": # Dwell time delay at the bottom of the hole
Drill.gcode += linenumber() + "G4 S" + str(drill_DwellTime) + "\n"
# Marlin uses P for milliseconds, S for seconds, change P to S
elif cmd == 'G83': # Peck drill cycle:
elif cmd == "G83": # Peck drill cycle:
chip_Space = drill_Step * 0.5
next_Stop_Z = drill_R - drill_Step
while next_Stop_Z >= drill_Z: