commit 64302c1b920323662aa1bd70757867629ccd6148 Author: Jörg Kurlbaum Date: Thu Aug 25 20:04:24 2022 +0200 Workbench for LaserCladding at KVT (initial commit) diff --git a/Init.py b/Init.py new file mode 100644 index 0000000..5875dea --- /dev/null +++ b/Init.py @@ -0,0 +1,5 @@ +# Workbench LaserCladding +import FreeCAD as App + +# add Import/Export types +#App.addExportType("Kuka KRL (*.src)", "exportKukaSrc") diff --git a/InitGui.py b/InitGui.py new file mode 100644 index 0000000..a26f968 --- /dev/null +++ b/InitGui.py @@ -0,0 +1,47 @@ +import FreeCAD as App + +class LaserCladding (Workbench): + MenuText = "Laser Cladding" + ToolTip = "Create Simple Path on workpieces for Laser Cladding" + Icon = """""" + + def __init__(self): + __dirname__ = os.path.join(FreeCAD.getUserAppDataDir(), "Mod", "LaserCladding") + _tooltip = "The LaserCladding Workbench can create Paths for Robots" + self.__class__.Icon = os.path.join(__dirname__, + "Resources", "icons", + "Laser_Workbench.svg") + + def Initialize(self): + """This function is executed when the workbench is first activated. + It is executed once in a FreeCAD session followed by the Activated function. + """ + import lasercladding + + self.list = ["CreateCladdingJob", "SelectBaseReference", "CreatePad"] # A list of command names created in the line above + self.appendToolbar("My Commands",self.list) # creates a new toolbar with your commands + self.appendMenu("LaserCladding",self.list) # creates a new menu + self.appendMenu(["LaserCladding","My submenu"],self.list) # appends a submenu to an existing menu + + def Activated(self): + """This function is executed whenever the workbench is activated""" + #from importlib import reload + #reload(lccmd) + return + + def Deactivated(self): + """This function is executed whenever the workbench is deactivated""" + return + + def ContextMenu(self, recipient): + """This function is executed whenever the user right-clicks on screen""" + # "recipient" will be either "view" or "tree" + self.appendContextMenu("My commands",self.list) # add commands to the context menu + + def GetClassName(self): + # This function is mandatory if this is a full Python workbench + # This is not a template, the returned string should be exactly "Gui::PythonWorkbench" + return "Gui::PythonWorkbench" + + +Gui.addWorkbench(LaserCladding()) diff --git a/Resources/LaserCladding.qrc b/Resources/LaserCladding.qrc new file mode 100644 index 0000000..5229187 --- /dev/null +++ b/Resources/LaserCladding.qrc @@ -0,0 +1,6 @@ + + + icons/Laser_Workbench.svg + + + diff --git a/Resources/icons/Laser_Workbench.svg b/Resources/icons/Laser_Workbench.svg new file mode 100644 index 0000000..a5c94df --- /dev/null +++ b/Resources/icons/Laser_Workbench.svg @@ -0,0 +1,351 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Mon Oct 10 13:44:52 2011 +0000 + + + [wmayer] + + + + + FreeCAD LGPL2+ + + + + + FreeCAD + + + FreeCAD/src/Mod/Draft/Resources/icons/Draft_2DShapeView.svg + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + [agryson] Alexander Gryson + + + + + box + plane + rectangle + + + A box floating above a projection of its lower face + + + + + + + + + + + + diff --git a/lasercladding/__init__.py b/lasercladding/__init__.py new file mode 100644 index 0000000..94f824b --- /dev/null +++ b/lasercladding/__init__.py @@ -0,0 +1,6 @@ +# module lasercladding + +from .commands import * +from .path import * +from .job import * +from .kuka import * diff --git a/lasercladding/commands.py b/lasercladding/commands.py new file mode 100644 index 0000000..5d363cf --- /dev/null +++ b/lasercladding/commands.py @@ -0,0 +1,77 @@ +import FreeCAD as App +import FreeCADGui as Gui +import Part + + +from .job import LaserJob, ViewProviderLaserJob +from .pad import LaserPad, create_laserpad + +class CreateCladdingJob(): + def Activated(self): + # Here your write what your ScriptCmd does... + App.Console.PrintMessage('Create Lasser Cladding Job') + a=App.ActiveDocument.addObject("App::FeaturePython","LaserJob") + LaserJob(a) + ViewProviderLaserJob(a.ViewObject) + + def GetResources(self): + return {'Pixmap' : 'path_to_an_icon/myicon.png', 'MenuText': 'Create Cladding Job', 'ToolTip': 'Add a Job to your Document'} + + +class SelectBaseReference(): + def Activated(self): + # Here your write what your ScriptCmd does... + App.Console.PrintMessage('Select Base reference!') + if not Gui.Selection.hasSelection(): + App.Console.PrintMessage('Select a Vertex') + return + # check length + selection = Gui.Selection.getSelectionEx() + # find first vertex + for s in selection: + if s.HasSubObjects: + for obj in s.SubObjects: + if isinstance(obj, Part.Vertex): + vertex = obj.copy() + laserjob_entry = App.ActiveDocument.getObject('LaserJob') + if laserjob_entry is None: + App.Console.PrintMessage('Create a LaserJob first') + return + laserjob_entry.base_reference = App.Vector((vertex.X, vertex.Y, vertex.Z)) + App.ActiveDocument.recompute() + + + def GetResources(self): + return {'Pixmap' : 'path_to_an_icon/myicon.png', 'MenuText': 'Select Base reference', 'ToolTip': 'Add a Job to your Document'} + + +class CreatePad(): + def Activated(self): + # Here your write what your ScriptCmd does... + App.Console.PrintMessage('Select some Face as reference') + if not Gui.Selection.hasSelection(): + App.Console.PrintMessage('Select a Face') + return + # check length + ref_face = (Gui.Selection.getSelection()[0], + Gui.Selection.getSelectionEx()[0].SubElementNames[0]) + #selection = Gui.Selection.getSelectionEx() + # find first vertex + #for s in selection: + # if s.HasSubObjects: + # for obj in s.SubObjects: + # if isinstance(obj, Part.Face): + # face = obj.copy() + create_laserpad(ref_face) + App.ActiveDocument.recompute() + + + def GetResources(self): + return {'Pixmap' : 'path_to_an_icon/myicon.png', 'MenuText': 'Select Base reference', 'ToolTip': 'Add a Job to your Document'} + + + + +Gui.addCommand('CreateCladdingJob', CreateCladdingJob()) +Gui.addCommand('SelectBaseReference', SelectBaseReference()) +Gui.addCommand('CreatePad', CreatePad()) diff --git a/lasercladding/job.py b/lasercladding/job.py new file mode 100644 index 0000000..ab334ce --- /dev/null +++ b/lasercladding/job.py @@ -0,0 +1,110 @@ +import FreeCAD as App + + +class LaserJob: + def __init__(self, obj): + '''Add some custom properties to our box feature''' + obj.addProperty("App::PropertyVector", "base_reference", "Reference", "Reference Point (teached)") + obj.base_reference = App.Vector(0,0,0) + + ## to make addObject() available + obj.addExtension("App::GroupExtensionPython", None) + obj.Proxy = self + + def addObject(self, obj): + self.pads = [obj] + + def onChanged(self, fp, prop): + '''Do something when a property has changed''' + App.Console.PrintMessage("Change property: " + str(prop) + "\n") + + def execute(self, fp): + '''Do something when doing a recomputation, this method is mandatory''' + App.Console.PrintMessage("Recompute Python Box feature\n") + + + + + +class ViewProviderLaserJob: + def __init__(self, obj): + '''Set this object to the proxy object of the actual view provider''' + obj.addProperty("App::PropertyColor","Color","Box","Color of the box").Color=(1.0,0.0,0.0) + + obj.addExtension("Gui::ViewProviderGroupExtensionPython", None) + + obj.Proxy = self + + def attach(self, obj): + '''Setup the scene sub-graph of the view provider, this method is mandatory''' + self.onChanged(obj,"Color") + + def updateData(self, fp, prop): + '''If a property of the handled feature has changed we have the chance to handle this here''' + # fp is the handled feature, prop is the name of the property that has changed + pass + + def getDisplayModes(self,obj): + '''Return a list of display modes.''' + modes=[] + modes.append("Shaded") + modes.append("Wireframe") + return modes + + def getDefaultDisplayMode(self): + '''Return the name of the default display mode. It must be defined in getDisplayModes.''' + return "Shaded" + + def setDisplayMode(self,mode): + '''Map the display mode defined in attach with those defined in getDisplayModes.\ + Since they have the same names nothing needs to be done. This method is optional''' + return mode + + def onChanged(self, vp, prop): + '''Here we can do something when a single property got changed''' + App.Console.PrintMessage("Change property: " + str(prop) + "\n") + #if prop == "Color": + # c = vp.getPropertyByName("Color") + # self.color.rgb.setValue(c[0],c[1],c[2]) + + def getIcon(self): + '''Return the icon in XPM format which will appear in the tree view. This method is\ + optional and if not defined a default icon is shown.''' + return """ + /* XPM */ + static const char * ViewProviderBox_xpm[] = { + "16 16 6 1", + " c None", + ". c #141010", + "+ c #615BD2", + "@ c #C39D55", + "# c #000000", + "$ c #57C355", + " ........", + " ......++..+..", + " .@@@@.++..++.", + " .@@@@.++..++.", + " .@@ .++++++.", + " ..@@ .++..++.", + "###@@@@ .++..++.", + "##$.@@$#.++++++.", + "#$#$.$$$........", + "#$$####### ", + "#$$#$$$$$# ", + "#$$#$$$$$# ", + "#$$#$$$$$# ", + " #$#$$$$$# ", + " ##$$$$$# ", + " ####### "}; + """ + + def __getstate__(self): + '''When saving the document this object gets stored using Python's json module.\ + Since we have some un-serializable parts here -- the Coin stuff -- we must define this method\ + to return a tuple of all serializable objects or None.''' + return None + + def __setstate__(self,state): + '''When restoring the serialized object from document we have the chance to set some internals here.\ + Since no data were serialized nothing needs to be done here.''' + return None diff --git a/lasercladding/kuka.py b/lasercladding/kuka.py new file mode 100644 index 0000000..08ad9b3 --- /dev/null +++ b/lasercladding/kuka.py @@ -0,0 +1,168 @@ +import FreeCAD +import numpy as np +import math +import time +import Part + + +TeachPointFold = """ +;FOLD LIN P4 Vel= 0.2 m/s CPDAT1 Tool[1] Base[0];%{PE}%R 5.4.27,%MKUKATPBASIS,%CMOVE,%VLIN,%P 1:LIN, 2:P4, 3:, 5:0.2, 7:CPDAT1 +$BWDSTART = FALSE +LDAT_ACT=LCPDAT1 +FDAT_ACT=FP4 +BAS(#CP_PARAMS,0.2) +LIN XP4 +;ENDFOLD +""" + +TeachPointDat = """ +DECL E6POS XP4={X -25.1844196,Y 1122.42603,Z 1158.07996,A -14.3267002,B 0.537901878,C 179.028305,S 6,T 59,E1 0.0,E2 0.0,E3 0.0,E4 0.0,E5 0.0,E6 0.0} +DECL FDAT FP4={TOOL_NO 1,BASE_NO 0,IPO_FRAME #BASE,POINT2[] " "} +DECL LDAT LCPDAT1={VEL 2.0,ACC 100.0,APO_DIST 100.0,APO_FAC 50.0,ORI_TYP #VAR} +""" + +header_src = """&ACCESS RVP +&REL 1 +&PARAM TEMPLATE = C:\KRC\Roboter\Template\ExpertVorgabe +&PARAM EDITMASK = * +""" + + + + +class Kuka_Prog: + def __init__(self): + self.pose_list = [] + self.base = (0,0,0) + + def append_pose(self, pose): + self.pose_list.append(pose) + + def append_poses(self, poses): + if not len(self.pose_list): + self.pose_list.extend(poses) + # poses are sorted + # but maybe we need to reverse + #get the point distance from first and last pose + last = self.pose_list[-1] + nfirst = poses[0] + nlast = poses[-1] + last = FreeCAD.Base.Vector(last.X, last.Y, last.Z) + nlast = FreeCAD.Base.Vector(nlast.X, nlast.Y, nlast.Z) + nfirst = FreeCAD.Base.Vector(nfirst.X, nfirst.Y, nfirst.Z) + dnl = last.distanceToPoint(nlast) + dnf = last.distanceToPoint(nfirst) + if dnl < dnf: + poses.reverse() + self.pose_list.extend(poses) + + def draw_wire(self): + path = Part.makePolygon([FreeCAD.Base.Vector(p.X, p.Y, p.Z) for p in self.pose_list ]) + s = Part.show(path) + s.ViewObject.LineColor=(1.0,0.5,0.0) + s.ViewObject.LineWidth=(2.5) + + def save_prog(self, filename): + srcfile = open(filename+'.src','w') + srcfile.write(header_src) + # subroutine definition + srcfile.write("DEF "+filename+"( )\n\n") + srcfile.write(";- Kuka src file, generated by KVT\n") + srcfile.write(";- "+ time.asctime()+"\n\n") + # defining world and base + srcfile.write(";------------- definitions ------------\n") + srcfile.write("EXT BAS (BAS_COMMAND :IN,REAL :IN ) ;set base to World\n") + srcfile.write("BAS (#INITMOV,0 ) ;Initialicing the defaults for Vel and so on \n\n") + + srcfile.write("$BASE = BASE_DATA[6] ; SET BASE to WORKPLACE\n\n") + srcfile.write("\n;------------- main part ------------\n") + + #V = w.Velocity / 1000.0 # from mm/s to m/s + V = 0.05 + srcfile.write("$VEL.CP = %f ; m/s ; m/s \n"%V) + for pose in self.pose_list: + srcfile.write("LIN {} ; GENERATED\n".format(pose.to_string())) + # end of subroutine + srcfile.write("\n;------------- end ------------\n") + srcfile.write("END \n\n") + srcfile.close() + +class Kuka_Pose: + def __init__(self): + self.X = 0.0 + self.Y = 0.0 + self.Z = 0.0 + self.A = 0.0 + self.B = 0.0 + self.C = 0.0 + + self.S = 0 + self.T = 0 + + self.E1 = 0.0 + self.E2 = 0.0 + + def set_from_point_and_normal(self, point, normal): + self.X = point.x + self.Y = point.y + self.Z = point.z + + r = FreeCAD.Base.Rotation(FreeCAD.Base.Vector(0,0,1), normal) + ABC_in_deg = r.toEulerAngles('ZYX') + self.A = math.radians(ABC_in_deg[0]) + self.B = math.radians(ABC_in_deg[1]) + self.C = math.radians(ABC_in_deg[2]) + #print("Rotation:", self.A, self.B, self.C) + + def from_point_and_normal(point, normal): + pose = Kuka_Pose() + pose.X = point.x + pose.Y = point.y + pose.Z = point.z + + r = FreeCAD.Base.Rotation(FreeCAD.Base.Vector(0,0,1), normal) + ABC_in_deg = r.toEulerAngles('ZYX') + pose.A = math.radians(ABC_in_deg[0]) + pose.B = math.radians(ABC_in_deg[1]) + pose.C = math.radians(ABC_in_deg[2]) + #print("Rotation:", self.A, self.B, self.C) + return pose + + def to_string(self): + pose_string="X {:.3f}, Y {:.3f}, Z {:.3f}, A {:.4f}, B {:.4f}, C {:.4f}, E1 {:.4f}, E2 {:.4f}" + return "{" + pose_string.format(self.X, self.Y, self.Z, self.A, self.B, self.C, self.E1, self.E2) + "}" + + def draw_pose(self): + #line=Part.makeLine(point, point+3*normal) + #lines.append(line) + # from euler to some line + # create upfacing vector then rotate around each axis? + up = FreeCAD.Base.Vector(0,0,1) + rotx = FreeCAD.Base.Rotation(FreeCAD.Base.Vector(1,0,0), math.degrees(self.C)) + roty = FreeCAD.Base.Rotation(FreeCAD.Base.Vector(0,1,0), math.degrees(self.B)) + rotz = FreeCAD.Base.Rotation(FreeCAD.Base.Vector(0,0,1), math.degrees(self.A)) + + rot = rotz.multiply(roty.multiply(rotx)) + up_rotated = rot.multVec(up) + + basepoint = FreeCAD.Base.Vector(self.X, self.Y, self.Z) + line = Part.makeLine(basepoint, basepoint+5*up_rotated) + #line.Placement.Rotation = rot + s = Part.show(line) + s.ViewObject.LineColor=(1.0,0.0,0.0) + + + +def get_list_of_poses(face, edge, n): + poses = [] + points = edge.discretize(n) + lines = [] + for point in points: + uv = face.Surface.parameter(point) + normal = face.normalAt(uv[0], uv[1]) + line=Part.makeLine(point, point+3*normal) + lines.append(line) + # create pose + pose = Kuka_Pose.from_point_and_normal(point, normal) + poses.append(pose) + return poses diff --git a/lasercladding/pad.py b/lasercladding/pad.py new file mode 100644 index 0000000..d24347a --- /dev/null +++ b/lasercladding/pad.py @@ -0,0 +1,149 @@ +import FreeCAD as App +import numpy as np +from . import kuka + + +class LaserPad: + def __init__(self, obj, face): + '''Add some custom properties to our box feature''' + obj.addProperty("App::PropertyEnumeration", "pathtype", "SlicingParameters", "Type of Path generation (slice, etc)") + obj.pathtype = ["sliced", "auto", "wire"] + + obj.addProperty("App::PropertyEnumeration", "slicedirection", "SlicingParameters", "When Slicing, which direction to use") + obj.slicedirection = ["xaxis", "yaxis", "zaxis", "custom"] + + obj.addProperty("App::PropertyLinkSub","ref_surface","SlicingParameters","reference_1") + obj.ref_surface = face + + obj.Proxy = self + + def onChanged(self, fp, prop): + '''Do something when a property has changed''' + App.Console.PrintMessage("Change property: " + str(prop) + "\n") + + def execute(self, fp): + '''Do something when doing a recomputation, this method is mandatory''' + App.Console.PrintMessage("Recompute LaserPad\n") + face = fp.ref_surface[0].getSubObject(fp.ref_surface[1])[0] + print(face) + + if fp.pathtype == "sliced": + xmin = face.BoundBox.XMin + xmax = face.BoundBox.XMax + ymin = face.BoundBox.YMin + ymax = face.BoundBox.YMax + slice_direction = App.Vector(0,0,0) + if fp.slicedirection == "xaxis": + slice_direction = App.Vector(1,0,0) + pmin, pmax = xmin, xmax + if fp.slicedirection == "yaxis": + slice_direction = App.Vector(0,1,0) + pmin, pmax = ymin, ymax + if fp.slicedirection == "zaxis": + slice_direction = App.Vector(0,0,1) + pmin, pmax = zmin, zmax + + slices = face.slices(slice_direction, [x for x in np.arange(pmin, pmax, 2)]) + + prog = kuka.Kuka_Prog() + for edge in slices.Edges: + poses = kuka.get_list_of_poses(face, edge, 10) + prog.append_poses(poses) + prog.draw_wire() + prog.save_prog("/home/jk/test_export_workbench") + + +### helper to create some object +def create_laserpad(face): + laserjob_entry = App.ActiveDocument.getObject('LaserJob') + if laserjob_entry is None: + App.Console.PrintMessage('Create a LaserJob first') + return + pad_obj = App.ActiveDocument.addObject("App::FeaturePython","LaserPad") + pad = LaserPad(pad_obj, face) + #a=laserjob_entry.addObbject(pad) + ViewProviderLaserPad(pad.ViewProvider) + return pad + + +class ViewProviderLaserPad: + def __init__(self, obj): + '''Set this object to the proxy object of the actual view provider''' + obj.addProperty("App::PropertyColor","Color","Box","Color of the box").Color=(1.0,0.0,0.0) + obj.Proxy = self + + def attach(self, obj): + '''Setup the scene sub-graph of the view provider, this method is mandatory''' + self.onChanged(obj,"Color") + + def updateData(self, fp, prop): + '''If a property of the handled feature has changed we have the chance to handle this here''' + # fp is the handled feature, prop is the name of the property that has changed + pass + + def getDisplayModes(self,obj): + '''Return a list of display modes.''' + modes=[] + modes.append("Shaded") + modes.append("Wireframe") + return modes + + def getDefaultDisplayMode(self): + '''Return the name of the default display mode. It must be defined in getDisplayModes.''' + return "Shaded" + + def setDisplayMode(self,mode): + '''Map the display mode defined in attach with those defined in getDisplayModes.\ + Since they have the same names nothing needs to be done. This method is optional''' + return mode + + def onChanged(self, vp, prop): + '''Here we can do something when a single property got changed''' + App.Console.PrintMessage("Change property: " + str(prop) + "\n") + #if prop == "Color": + # c = vp.getPropertyByName("Color") + # self.color.rgb.setValue(c[0],c[1],c[2]) + + def getIcon(self): + '''Return the icon in XPM format which will appear in the tree view. This method is\ + optional and if not defined a default icon is shown.''' + return """ + /* XPM */ + static const char * ViewProviderBox_xpm[] = { + "16 16 6 1", + " c None", + ". c #141010", + "+ c #615BD2", + "@ c #C39D55", + "# c #000000", + "$ c #57C355", + " ........", + " ......++..+..", + " .@@@@.++..++.", + " .@@@@.++..++.", + " .@@ .++++++.", + " ..@@ .++..++.", + "###@@@@ .++..++.", + "##$.@@$#.++++++.", + "#$#$.$$$........", + "#$$####### ", + "#$$#$$$$$# ", + "#$$#$$$$$# ", + "#$$#$$$$$# ", + " #$#$$$$$# ", + " ##$$$$$# ", + " ####### "}; + """ + + def __getstate__(self): + '''When saving the document this object gets stored using Python's json module.\ + Since we have some un-serializable parts here -- the Coin stuff -- we must define this method\ + to return a tuple of all serializable objects or None.''' + return None + + def __setstate__(self,state): + '''When restoring the serialized object from document we have the chance to set some internals here.\ + Since no data were serialized nothing needs to be done here.''' + return None + + diff --git a/lasercladding/path.py b/lasercladding/path.py new file mode 100644 index 0000000..b702db7 --- /dev/null +++ b/lasercladding/path.py @@ -0,0 +1,16 @@ +import FreeCAD as App + +class LaserPath: + def __init__(self, obj): + '''Add some custom properties to our box feature''' + obj.addProperty("App::Property", "base_reference", "Reference", "Reference Point (teached)") + obj.base_reference = App.Vector(0,0,0) + obj.Proxy = self + + def onChanged(self, fp, prop): + '''Do something when a property has changed''' + App.Console.PrintMessage("Change property: " + str(prop) + "\n") + + def execute(self, fp): + '''Do something when doing a recomputation, this method is mandatory''' + App.Console.PrintMessage("Recompute Python Box feature\n")