@ -1,47 +0,0 @@ |
|||||||
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()) |
|
||||||
@ -1,6 +0,0 @@ |
|||||||
<RCC> |
|
||||||
<qresource> |
|
||||||
<file>icons/Laser_Workbench.svg</file> |
|
||||||
<!-- <file>ui/preferences-draft.ui</file> --> |
|
||||||
</qresource> |
|
||||||
</RCC> |
|
||||||
@ -0,0 +1,11 @@ |
|||||||
|
<RCC> |
||||||
|
<qresource> |
||||||
|
<file>icons/Laser_Workbench.svg</file> |
||||||
|
<file>icons/LaserCreatePad.svg</file> |
||||||
|
<file>icons/LaserCreateProg.svg</file> |
||||||
|
<file>icons/LaserRecomputePad.svg</file> |
||||||
|
<file>icons/LaserSaveProg.svg</file> |
||||||
|
<file>icons/LaserSelectBaseRef.svg</file> |
||||||
|
<!-- <file>uiLaser_Workbench.svg/preferences-draft.ui</file> --> |
||||||
|
</qresource> |
||||||
|
</RCC> |
||||||
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
@ -0,0 +1,170 @@ |
|||||||
|
import FreeCAD as App |
||||||
|
import FreeCADGui as Gui |
||||||
|
import Part |
||||||
|
import os |
||||||
|
import re |
||||||
|
import copy |
||||||
|
|
||||||
|
from .program import LaserProgram, ViewProviderLaserProgram |
||||||
|
from .pad import LaserPad, ViewProviderLaserPad |
||||||
|
from .kuka import Kuka_Prog, Kuka_Pose, get_list_of_poses |
||||||
|
|
||||||
|
|
||||||
|
class LCCreateProgram(): |
||||||
|
def Activated(self): |
||||||
|
# Here your write what your ScriptCmd does... |
||||||
|
App.Console.PrintMessage('Create Lasser Cladding Program') |
||||||
|
a=App.ActiveDocument.addObject("App::FeaturePython","LaserProgram") |
||||||
|
LaserProgram(a) |
||||||
|
ViewProviderLaserProgram(a.ViewObject) |
||||||
|
|
||||||
|
def GetResources(self): |
||||||
|
return {'Pixmap' : ":icons/LaserCreateProg.svg", 'MenuText': 'Create Laser Program', 'ToolTip': 'Add a Laser Program to your Document'} |
||||||
|
|
||||||
|
|
||||||
|
class LCSelectBaseReference(): |
||||||
|
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('LaserProgram') |
||||||
|
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' : ":icons/LaserSelectBaseRef.svg", |
||||||
|
'MenuText': 'Select Base reference', |
||||||
|
'ToolTip': 'Add a Job to your Document'} |
||||||
|
|
||||||
|
|
||||||
|
class LCCreatePad(): |
||||||
|
|
||||||
|
def _create_laserpad(self, ref_to_face): |
||||||
|
laserprogram = App.ActiveDocument.getObject('LaserProgram') |
||||||
|
if laserprogram is None: |
||||||
|
App.Console.PrintMessage('Create a LaserProgram first') |
||||||
|
return |
||||||
|
pad_obj = laserprogram.newObject("App::FeaturePython","LaserPad") |
||||||
|
LaserPad(pad_obj, ref_to_face) |
||||||
|
ViewProviderLaserPad(pad_obj.ViewObject) |
||||||
|
return pad_obj |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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() |
||||||
|
self._create_laserpad(ref_face) |
||||||
|
App.ActiveDocument.recompute() |
||||||
|
|
||||||
|
|
||||||
|
def GetResources(self): |
||||||
|
return {'Pixmap' : ":icons/LaserCreatePad.svg", 'MenuText': 'Create Pad', 'ToolTip': 'Create a Pad on selected face'} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class LCRecompute(): |
||||||
|
|
||||||
|
def Activated(self): |
||||||
|
# Here your write what your ScriptCmd does... |
||||||
|
App.Console.PrintMessage('Recomputer Pads') |
||||||
|
if not Gui.Selection.hasSelection(): |
||||||
|
App.Console.PrintMessage('Select a Pad') |
||||||
|
return |
||||||
|
|
||||||
|
if Gui.Selection.hasSelection(): |
||||||
|
laser_path_obj = Gui.Selection.getSelection()[0] |
||||||
|
if len(laser_path_obj.Group): |
||||||
|
laser_path_obj.removeObjectsFromDocument() |
||||||
|
laser_path_obj.Proxy._create_path(laser_path_obj) |
||||||
|
App.ActiveDocument.recompute() |
||||||
|
|
||||||
|
|
||||||
|
def GetResources(self): |
||||||
|
return {'Pixmap' : ":icons/LaserRecomputePad.svg", |
||||||
|
'MenuText': 'Recompute', |
||||||
|
'ToolTip': 'Recompute selected Pads'} |
||||||
|
|
||||||
|
|
||||||
|
class LCSaveProg(): |
||||||
|
|
||||||
|
def Activated(self): |
||||||
|
# Here your write what your ScriptCmd does... |
||||||
|
App.Console.PrintMessage('Saving to KRL') |
||||||
|
c = App.ActiveDocument.getObject("LaserProgram") |
||||||
|
pads = c.Group |
||||||
|
prog = Kuka_Prog() |
||||||
|
|
||||||
|
for pad in pads: |
||||||
|
# one pad with contours and hatchlines |
||||||
|
for progpart in pad.Group: |
||||||
|
## jedes Contour oder Hatchline Feature |
||||||
|
if re.match('Contour*', progpart.Name): |
||||||
|
# do Conoutes |
||||||
|
face = pad.ref_body.getSubObject(pad.ref_surface) |
||||||
|
edges = Part.__sortEdges__(progpart.Shape.Edges) |
||||||
|
vlist = [] |
||||||
|
for edge in edges: |
||||||
|
vlist.extend([v.Point for v in edge.Vertexes]) |
||||||
|
poly = Part.makePolygon(vlist) |
||||||
|
#Part.show(poly, "ContourPath") |
||||||
|
poses = get_list_of_poses(face, poly.Edges) |
||||||
|
prog.append_contour(poses, progpart.pathtype) |
||||||
|
elif re.match('Hatch*', progpart.Name): |
||||||
|
face = pad.ref_body.getSubObject(pad.ref_surface) |
||||||
|
edges = progpart.Shape.Edges |
||||||
|
for edge in edges: |
||||||
|
p0 = edge.Vertexes[0].Point |
||||||
|
p1 = edge.Vertexes[1].Point |
||||||
|
line = [] |
||||||
|
for p in [p0, p1]: |
||||||
|
uv = face.Surface.parameter(p) |
||||||
|
normal = face.normalAt(uv[0], uv[1]) |
||||||
|
pose = Kuka_Pose.from_point_and_normal(p, normal) |
||||||
|
line.append(pose) |
||||||
|
prog.append_hatchline(line, progpart.pathtype) |
||||||
|
|
||||||
|
prog.set_base(c.base_reference) |
||||||
|
prog.save_prog(c.progpath) |
||||||
|
App.ActiveDocument.recompute() |
||||||
|
|
||||||
|
|
||||||
|
def GetResources(self): |
||||||
|
return {'Pixmap' : ":icons/LaserSaveProg.svg", |
||||||
|
'MenuText': 'Save Program', |
||||||
|
'ToolTip': 'Save the Program as KRL'} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Gui.addCommand('LCCreateProgram', LCCreateProgram()) |
||||||
|
Gui.addCommand('LCSelectBaseReference', LCSelectBaseReference()) |
||||||
|
Gui.addCommand('LCCreatePad', LCCreatePad()) |
||||||
|
Gui.addCommand('LCRecompute', LCRecompute()) |
||||||
|
Gui.addCommand('LCSaveProg', LCSaveProg()) |
||||||
@ -0,0 +1,63 @@ |
|||||||
|
import FreeCAD as App |
||||||
|
import FreeCADGui as Gui |
||||||
|
from freecad.LaserCladdingWorkbench import lc_resource |
||||||
|
|
||||||
|
from freecad.LaserCladdingWorkbench import commands |
||||||
|
|
||||||
|
import os |
||||||
|
|
||||||
|
class LaserCladding (Gui.Workbench): |
||||||
|
MenuText = "Laser Cladding" |
||||||
|
ToolTip = "Create Simple Paths on workpieces for Laser Cladding" |
||||||
|
Icon = ":/icons/Laser_Workbench.svg" |
||||||
|
|
||||||
|
|
||||||
|
def __init__(self): |
||||||
|
pass |
||||||
|
|
||||||
|
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 freecad.LaserCladdingWorkbench |
||||||
|
self.appendMenu("LaserCladding", |
||||||
|
["LCCreateProgram", |
||||||
|
"LCSelectBaseReference", |
||||||
|
"LCCreatePad", |
||||||
|
"LCRecompute", |
||||||
|
"LCSaveProg" |
||||||
|
] |
||||||
|
) # creates a new menu |
||||||
|
|
||||||
|
self.appendToolbar("LaserCladding", |
||||||
|
["LCCreateProgram", |
||||||
|
"LCSelectBaseReference", |
||||||
|
"LCCreatePad", |
||||||
|
"LCRecompute", |
||||||
|
"LCSaveProg" |
||||||
|
] |
||||||
|
) # creates a new toolbar |
||||||
|
|
||||||
|
def Activated(self): |
||||||
|
"""This function is executed whenever the workbench is activated""" |
||||||
|
import freecad.LaserCladdingWorkbench |
||||||
|
from importlib import reload |
||||||
|
reload(freecad.LaserCladdingWorkbench) |
||||||
|
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("Laser Cladding",[]) # 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()) |
||||||
@ -0,0 +1,265 @@ |
|||||||
|
import FreeCAD |
||||||
|
import numpy as np |
||||||
|
import math |
||||||
|
import time |
||||||
|
import Part |
||||||
|
import re |
||||||
|
import copy |
||||||
|
|
||||||
|
|
||||||
|
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 = * |
||||||
|
""" |
||||||
|
|
||||||
|
ptp_fold = """;FOLD PTP xp1 Vel=100 % PDAT1 Tool[6]:laser6 Base[2]:Laser;%{PE}%R 8.2.24,%MKUKATPBASIS,%CMOVE,%VPTP,%P 1:PTP, 2:xp1, 3:, 5:100, 7:PDAT1 |
||||||
|
$BWDSTART=FALSE |
||||||
|
PDAT_ACT=PPDAT1 |
||||||
|
FDAT_ACT=Fxp1 |
||||||
|
BAS(#PTP_PARAMS,100) |
||||||
|
PTP Xxp1 |
||||||
|
;ENDFOLD""" |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Kuka_Prog: |
||||||
|
def __init__(self): |
||||||
|
self.contour_path_list = [] |
||||||
|
self.hatchlines_list = [] |
||||||
|
self.base = (0,0,0) |
||||||
|
|
||||||
|
def set_base(self, vec): |
||||||
|
self.base = (vec.x, vec.y, vec.z) |
||||||
|
|
||||||
|
def append_contour(self, poses, segmenttype = 'LIN'): |
||||||
|
self.contour_path_list.append((poses, segmenttype)) |
||||||
|
|
||||||
|
def append_hatchline(self, line, segmenttype = 'LIN'): |
||||||
|
if not len(self.hatchlines_list): |
||||||
|
self.hatchlines_list.append((line, segmenttype)) |
||||||
|
# poses are sorted |
||||||
|
# but maybe we need to reverse |
||||||
|
#get the point distance from first and last pose |
||||||
|
last, _ = self.hatchlines_list[-1] |
||||||
|
nfirst = line[0] |
||||||
|
nlast = line[-1] |
||||||
|
last = FreeCAD.Base.Vector(last[1].X, last[1].Y, last[1].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: |
||||||
|
line.reverse() |
||||||
|
self.hatchlines_list.append((line, segmenttype)) |
||||||
|
|
||||||
|
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, obj): |
||||||
|
path = Part.makePolygon([FreeCAD.Base.Vector(p.X, p.Y, p.Z) for p in self.pose_list ]) |
||||||
|
#s = Part.show(path) |
||||||
|
obj.addObject(s) |
||||||
|
s.ViewObject.LineColor=(1.0,0.5,0.0) |
||||||
|
s.ViewObject.LineWidth=(2.5) |
||||||
|
|
||||||
|
def get_vectors(self): |
||||||
|
return [FreeCAD.Base.Vector(p.X, p.Y, p.Z) for p in poses for poses in self.contour_path_list ] |
||||||
|
|
||||||
|
def save_prog(self, filename): |
||||||
|
if not filename.endswith('.src'): |
||||||
|
filename = filename +'.src' |
||||||
|
srcfile = open(filename, '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("E6POS startp\n") |
||||||
|
# srcfile.write("DECL E6AXIS xp1={A1 -1.9, A2 -105.76, A3 79.97, A4 178.83, A5 -20.3, A6 -4.37, E1 -90, E2 0}\n") |
||||||
|
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("BAS (#TOOL,6) ;Initialicing the defaults for Vel and so on \n\n") |
||||||
|
srcfile.write("BAS (#BASE,2) ;Initialicing the defaults for Vel and so on \n\n") |
||||||
|
#srcfile.write(ptp_fold) |
||||||
|
srcfile.write("PTP {A1 -33.31, A2 -104.71, A3 114.60, A4 282.66, A5 -39.21, A6 -104.87, E1 -90, E2 1.0}\n") |
||||||
|
srcfile.write("\n;------------- main part ------------\n") |
||||||
|
srcfile.write("startp=$POS_ACT\n") |
||||||
|
#V = w.Velocity / 1000.0 # from mm/s to m/s |
||||||
|
V_prozess = 0.0225 |
||||||
|
V_max = 0.15 |
||||||
|
CDIS = 2.3 |
||||||
|
CVEL = 95.0 |
||||||
|
LASERPOWER = 0.4 |
||||||
|
srcfile.write("$VEL.CP = %f ; m/s ; m/s \n"%V_max) |
||||||
|
srcfile.write("$APO.CDIS = %f ; mm \n"%CDIS) |
||||||
|
srcfile.write("$APO.CVEL = %f ; percent \n"%CVEL) |
||||||
|
srcfile.write("$ANOUT[1] = %f ; \n"%LASERPOWER) |
||||||
|
srcfile.write("$OUT[7] = TRUE ; \n") |
||||||
|
srcfile.write("$OUT[9] = TRUE ; \n") |
||||||
|
srcfile.write("LIN startp:{X -100.0, Y 0.0, Z 0.0, A 0.0000, B 0.0000, C 0.0000, E1 0.0000, E2 0.0000} C_VEL; GENERATED\n") |
||||||
|
srcfile.write("WAIT SEC 10.0\n") |
||||||
|
srcfile.write(";- Contourpaths\n") |
||||||
|
for (poses, seg_type) in self.contour_path_list: |
||||||
|
# start laser code |
||||||
|
srcfile.write("$VEL.CP = %f ; m/s ; m/s \n"%V_prozess) |
||||||
|
srcfile.write(";- Turn on Laser\n") |
||||||
|
if seg_type == 'LIN': |
||||||
|
srcfile.write("LIN startp:{} C_VEL; GENERATED\n".format(poses[0].translate_with(self.base).to_string())) |
||||||
|
srcfile.write("TRIGGER WHEN DISTANCE=0 DELAY=0 DO $OUT[3]=True\n" ) ## Einschalten |
||||||
|
for pose in poses[1:]: |
||||||
|
srcfile.write("LIN startp:{} C_VEL; GENERATED\n".format(pose.translate_with(self.base).to_string())) |
||||||
|
|
||||||
|
if seg_type == 'SPLINE': |
||||||
|
srcfile.write("SPLINE\n") |
||||||
|
for pose in poses: |
||||||
|
srcfile.write(" SPL startp:{} ; GENERATED\n".format(pose.translate_with(self.base).to_string())) |
||||||
|
srcfile.write("ENDSPLINE\n") |
||||||
|
|
||||||
|
srcfile.write(";- Turn off Laser\n") |
||||||
|
srcfile.write("$OUT[3] = FALSE\n") |
||||||
|
# end of subroutine |
||||||
|
|
||||||
|
srcfile.write(";- Hatchlines\n") |
||||||
|
for (line, seg_type) in self.hatchlines_list: |
||||||
|
# start laser code |
||||||
|
srcfile.write(";- Hatchline\n") |
||||||
|
if seg_type == 'LIN': |
||||||
|
srcfile.write("$VEL.CP = %f ; m/s ; m/s \n"%V_max) |
||||||
|
srcfile.write("LIN startp:{} C_VEL; GENERATED\n".format(line[0].translate_with(self.base).to_string())) |
||||||
|
srcfile.write("TRIGGER WHEN DISTANCE=0 DELAY=0 DO $OUT[3]=True\n" ) ## Einschalten |
||||||
|
srcfile.write("$VEL.CP = %f ; m/s ; m/s \n"%V_prozess) |
||||||
|
srcfile.write("LIN startp:{} C_VEL; GENERATED\n".format(line[1].translate_with(self.base).to_string())) |
||||||
|
srcfile.write("TRIGGER WHEN DISTANCE=0 DELAY=0 DO $OUT[3]=FALSE\n") ## Ausschalten |
||||||
|
# end of subroutine |
||||||
|
srcfile.write("$OUT[3] = FALSE\n") |
||||||
|
srcfile.write("$OUT[7] = FALSE ; \n") |
||||||
|
srcfile.write("$OUT[9] = FALSE ; \n") |
||||||
|
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, rot=False): |
||||||
|
if rot: |
||||||
|
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) + "}" |
||||||
|
else: |
||||||
|
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, 0,0,0,0,0) + "}" |
||||||
|
|
||||||
|
|
||||||
|
def translate_with(self, vector): |
||||||
|
pose = copy.copy(self) |
||||||
|
pose.X = pose.X - vector[0] |
||||||
|
pose.Y = pose.Y - vector[1] |
||||||
|
pose.Z = pose.Z - vector[2] |
||||||
|
return pose |
||||||
|
|
||||||
|
|
||||||
|
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, edges): |
||||||
|
poses = [] |
||||||
|
for edge in edges: |
||||||
|
p0 = edge.Vertexes[0].Point |
||||||
|
p1 = edge.Vertexes[1].Point |
||||||
|
|
||||||
|
for p in [p0, p1]: |
||||||
|
uv = face.Surface.parameter(p) |
||||||
|
normal = face.normalAt(uv[0], uv[1]) |
||||||
|
pose = Kuka_Pose.from_point_and_normal(p, normal) |
||||||
|
poses.append(pose) |
||||||
|
return poses |
||||||
@ -0,0 +1,195 @@ |
|||||||
|
import FreeCAD |
||||||
|
import FreeCAD as App |
||||||
|
import FreeCADGui as Gui |
||||||
|
import numpy as np |
||||||
|
import math |
||||||
|
import Part |
||||||
|
from shapely.geometry import Polygon, Point |
||||||
|
from pyslm import hatching as hatching |
||||||
|
from pyslm.geometry.geometry import LayerGeometryType |
||||||
|
from typing import List |
||||||
|
import rdp |
||||||
|
from .utils import * |
||||||
|
from freecad.LaserCladdingWorkbench.path import LaserPath |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class LaserPad: |
||||||
|
def __init__(self, obj, face): |
||||||
|
obj.addProperty("App::PropertyFloatConstraint", "tool_width", "Parameters", "Width of the circular effect area of Laser in mm") |
||||||
|
obj.tool_width = (2.3, 0.01, 20.0, 0.1) |
||||||
|
|
||||||
|
obj.addProperty("App::PropertyFloatConstraint", "hatch_angle", "Hatching Parameters", "Hatch Angle") |
||||||
|
obj.hatch_angle = (0.0, 0.0, 180.0, 1.0) |
||||||
|
|
||||||
|
obj.addProperty("App::PropertyFloat", "hatch_volume_offset", "Parameters", "Offset between internal and external boundary") |
||||||
|
obj.hatch_volume_offset = 2.3*0.5 |
||||||
|
|
||||||
|
obj.addProperty("App::PropertyFloat", "hatch_spot_compensation", "Parameters", "Additional offset to account for laser spot size") |
||||||
|
obj.hatch_spot_compensation = 2.3*0.5 |
||||||
|
|
||||||
|
obj.addProperty("App::PropertyInteger", "hatch_inner_contours", "Parameters", "Inner Contours") |
||||||
|
obj.hatch_inner_contours = 1 |
||||||
|
|
||||||
|
obj.addProperty("App::PropertyInteger", "hatch_outer_contours", "Parameters", "Outer Contours") |
||||||
|
obj.hatch_outer_contours = 0 |
||||||
|
|
||||||
|
obj.addProperty("App::PropertyFloat", "hatch_contour_offset", "Parameters", "Contour Offset") |
||||||
|
obj.hatch_contour_offset = 2.3 |
||||||
|
|
||||||
|
obj.addProperty("App::PropertyLinkGlobal","ref_body","Reference","body") |
||||||
|
obj.ref_body = face[0] |
||||||
|
|
||||||
|
obj.addProperty("App::PropertyString","ref_surface","Reference","face") |
||||||
|
obj.ref_surface = face[1] |
||||||
|
|
||||||
|
obj.addExtension("App::GroupExtensionPython") |
||||||
|
obj.Proxy = self |
||||||
|
self._create_path(obj) |
||||||
|
|
||||||
|
def _create_path(self, obj): |
||||||
|
print("Create_path") |
||||||
|
print("obj.ref_surface", obj.ref_surface) |
||||||
|
# if len(obj.Group): |
||||||
|
# obj.removeObjectsFromDocument() |
||||||
|
face = obj.ref_body.getSubObject(obj.ref_surface) |
||||||
|
myHatcher = hatching.Hatcher() |
||||||
|
myHatcher.hatchDistance = obj.tool_width |
||||||
|
myHatcher.hatchAngle = obj.hatch_angle |
||||||
|
myHatcher.volumeOffsetHatch = obj.hatch_volume_offset |
||||||
|
myHatcher.spotCompensation = obj.hatch_spot_compensation |
||||||
|
myHatcher.numInnerContours = obj.hatch_inner_contours |
||||||
|
myHatcher.numOuterContours = obj.hatch_outer_contours |
||||||
|
myHatcher.contourOffset = obj.hatch_contour_offset |
||||||
|
myHatcher.hatchSortMethod = hatching.AlternateSort() |
||||||
|
|
||||||
|
polygon_coords = get_coords_from_shape(face) |
||||||
|
layer = myHatcher.hatch(polygon_coords) |
||||||
|
|
||||||
|
contours_geoms = layer.getContourGeometry() |
||||||
|
hatch_geoms = layer.getHatchGeometry() |
||||||
|
|
||||||
|
contours = create_contour_lines(contours_geoms) |
||||||
|
hatchlines = create_hatch_lines(hatch_geoms) |
||||||
|
|
||||||
|
proj_contours = project_to_face(contours, face) |
||||||
|
proj_hatchlines = project_to_face(hatchlines, face) |
||||||
|
# DEBUG Part.show(Part.makeCompound(hatchlines)) |
||||||
|
obj.purgeTouched() |
||||||
|
|
||||||
|
for pc in proj_contours: |
||||||
|
p = Part.makeCompound(pc) |
||||||
|
contours_comp = obj.newObject("Part::Feature", "Contours") |
||||||
|
LaserPath(contours_comp) |
||||||
|
contours_comp.Shape = p |
||||||
|
contours_comp.ViewObject.LineColor = (0.5, 0.8, 0.9) |
||||||
|
|
||||||
|
print("Creating Hatch Part") |
||||||
|
hatches_comp = obj.newObject("Part::Feature", "Hatchlines") |
||||||
|
p = Part.makeCompound([]) |
||||||
|
for pc in proj_hatchlines: |
||||||
|
p.add(Part.makeCompound(pc)) |
||||||
|
|
||||||
|
LaserPath(hatches_comp) |
||||||
|
hatches_comp.Shape = p |
||||||
|
hatches_comp.ViewObject.LineColor = (0.9, 0.2, 0.2) |
||||||
|
print("Group: ", obj.Group) |
||||||
|
|
||||||
|
def onChanged(self, fp, prop): |
||||||
|
'''Do something when a property has changed''' |
||||||
|
App.Console.PrintMessage("Change property: " + str(prop) + "\n") |
||||||
|
|
||||||
|
def execute(self, fp): |
||||||
|
App.Console.PrintMessage("Recompute LaserPad\n") |
||||||
|
# self._create_path(fp) |
||||||
|
|
||||||
|
|
||||||
|
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.addExtension("Gui::ViewProviderGroupExtensionPython") |
||||||
|
self.Object = obj.Object |
||||||
|
obj.Proxy = self |
||||||
|
|
||||||
|
def claimChildren(self): |
||||||
|
"""Return objects that will be placed under it in the tree view.""" |
||||||
|
App.Console.PrintMessage("Reclaim children\n") |
||||||
|
|
||||||
|
claimed = [] |
||||||
|
App.Console.PrintMessage(self.__dict__) |
||||||
|
for o in self.Object.OutList: |
||||||
|
if o.TypeId == 'Part::Feature': |
||||||
|
claimed.append(o) |
||||||
|
return claimed |
||||||
|
|
||||||
|
def attach(self, vobj): |
||||||
|
self.Object = vobj.Object |
||||||
|
self.ViewObject = vobj |
||||||
|
|
||||||
|
def getDisplayModes(self, obj): |
||||||
|
"'''Return a list of display modes.'''" |
||||||
|
return ["Standard"] |
||||||
|
|
||||||
|
def getDefaultDisplayMode(self): |
||||||
|
"'''Return the name of the default display mode. It must be defined in getDisplayModes.'''" |
||||||
|
return "Standard" |
||||||
|
|
||||||
|
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 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 |
||||||
@ -0,0 +1,17 @@ |
|||||||
|
import FreeCAD as App |
||||||
|
import FreeCADGui as Gui |
||||||
|
|
||||||
|
class LaserPath: |
||||||
|
def __init__(self, obj): |
||||||
|
obj.addProperty("App::PropertyEnumeration", "pathtype", "Parameter", "How this path is converted to robot") |
||||||
|
obj.pathtype = ["LIN", "CIRC", "SPLINE"] |
||||||
|
|
||||||
|
# obj.addExtension("App::GroupExtensionPython") |
||||||
|
# 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): |
||||||
|
App.Console.PrintMessage("Recompute LaserPath\n") |
||||||
@ -0,0 +1,160 @@ |
|||||||
|
import FreeCAD |
||||||
|
import FreeCAD as App |
||||||
|
import FreeCADGui as Gui |
||||||
|
import numpy as np |
||||||
|
import math |
||||||
|
import Part |
||||||
|
from shapely.geometry import Polygon, Point |
||||||
|
from pyslm import hatching as hatching |
||||||
|
from pyslm.geometry.geometry import LayerGeometryType |
||||||
|
from typing import List |
||||||
|
import rdp |
||||||
|
|
||||||
|
def project_to_face(compounds, face): |
||||||
|
# proj = Part.makeCompound([]) |
||||||
|
proj = [] |
||||||
|
for c in compounds: |
||||||
|
projection_result = [] |
||||||
|
for e in c.Edges: |
||||||
|
projection_result.append(face.makeParallelProjection(e, App.Vector(0, 0, 1))) |
||||||
|
# proj.add(Part.makeCompound(projection_result)) |
||||||
|
proj.append(projection_result) |
||||||
|
return proj |
||||||
|
|
||||||
|
|
||||||
|
def map_wire(wire, surface): |
||||||
|
"""Map wire on target surface |
||||||
|
Input wire must be on XY plane""" |
||||||
|
plane = Part.Plane().toShape() |
||||||
|
mapped_edges = [] |
||||||
|
for e in wire.Edges: |
||||||
|
c, fp, lp = plane.curveOnSurface(e) |
||||||
|
mapped_edges.append(c.toShape(surface, fp, lp)) |
||||||
|
return Part.Wire(mapped_edges) |
||||||
|
|
||||||
|
|
||||||
|
def path2DToPathList(shapes: List[Polygon]) -> List[np.ndarray]: |
||||||
|
""" |
||||||
|
Returns the list of paths and coordinates from a cross-section (i.e. Trimesh Path2D). This is required to be |
||||||
|
done for performing boolean operations and offsetting with the internal PyClipper package. |
||||||
|
:param shapes: A list of :class:`shapely.geometry.Polygon` representing a cross-section or container of |
||||||
|
closed polygons |
||||||
|
:return: A list of paths (Numpy Coordinate Arrays) describing fully closed and oriented paths. |
||||||
|
""" |
||||||
|
paths = [] |
||||||
|
|
||||||
|
for poly in shapes: |
||||||
|
coords = np.array(poly.exterior.coords) |
||||||
|
paths.append(coords) |
||||||
|
|
||||||
|
for path in poly.interiors: |
||||||
|
coords = np.array(path.coords) |
||||||
|
paths.append(coords) |
||||||
|
|
||||||
|
return paths |
||||||
|
|
||||||
|
|
||||||
|
def get_polypoints_from_edges(edges): |
||||||
|
# print("START NEW POLYGON") |
||||||
|
poly_points = [] |
||||||
|
# print("edges:", len(edges)) |
||||||
|
for edge in Part.__sortEdges__(edges): |
||||||
|
if type(edge.Curve) is Part.Circle and edge.Closed: |
||||||
|
# print("Edge is Circle") |
||||||
|
c = edge.Curve |
||||||
|
center = Point(c.Center.x, c.Center.y) |
||||||
|
radius = c.Radius |
||||||
|
circle = center.buffer(radius) |
||||||
|
poly_points = circle.exterior.coords |
||||||
|
elif type(edge.Curve) in [Part.Ellipse, Part.BSplineCurve, Part.Circle]: |
||||||
|
# print("Edge is Ellipse, BSpline or unclosed Circle") |
||||||
|
n = math.floor(edge.Length/2.3) |
||||||
|
if n > 200: |
||||||
|
n = 200 |
||||||
|
if edge.Closed: |
||||||
|
# print("Edge is closed Ellipse or BSpline") |
||||||
|
for v in edge.discretize(Number=n): |
||||||
|
poly_points.append((v.x, v.y)) |
||||||
|
else: |
||||||
|
# print("Edge is unclosed Circle") |
||||||
|
# for Circles at least 64 steps for full circle |
||||||
|
for v in edge.discretize(Number=n, First=edge.FirstParameter, Last=edge.LastParameter): |
||||||
|
poly_points.append((v.x, v.y)) |
||||||
|
else: |
||||||
|
# print("Line Vertexes:", len(edge.Vertexes)) |
||||||
|
# print("Line Vertexes:", [(v.Point.x, v.Point.y) for v in edge.Vertexes]) |
||||||
|
for v in edge.Vertexes: |
||||||
|
poly_points.append((v.Point.x, v.Point.y)) |
||||||
|
u = np.unique(poly_points, axis=0) |
||||||
|
# print("UNIQUE:", u) |
||||||
|
# print("END NEW POLYGON") |
||||||
|
return poly_points |
||||||
|
|
||||||
|
|
||||||
|
def tuple_is_equal(t1, t2): |
||||||
|
if math.isclose(t1[0], t2[0]) and math.isclose(t1[1], t2[1]): |
||||||
|
return True |
||||||
|
return False |
||||||
|
|
||||||
|
|
||||||
|
def get_polygon_from_subshape(subshape): |
||||||
|
# print("START NEW POLYGON") |
||||||
|
polygon = [] |
||||||
|
for edge in Part.__sortEdges__(subshape.Edges): |
||||||
|
poly_points = [] |
||||||
|
if type(edge.Curve) in [Part.Ellipse, Part.BSplineCurve, Part.Circle]: |
||||||
|
n = math.floor(edge.Length/2.3) |
||||||
|
for v in edge.discretize(Number=n, First=edge.FirstParameter, Last=edge.LastParameter): |
||||||
|
poly_points.append((v.x, v.y)) |
||||||
|
else: |
||||||
|
# print("Line Vertexes:", len(edge.Vertexes)) |
||||||
|
# print("Line Vertexes:", [(v.Point.x, v.Point.y) for v in edge.Vertexes]) |
||||||
|
for v in edge.Vertexes: |
||||||
|
poly_points.append((v.Point.x, v.Point.y)) |
||||||
|
|
||||||
|
if len(polygon): |
||||||
|
if tuple_is_equal(poly_points[0], polygon[-1]): |
||||||
|
polygon.extend(poly_points[1:]) |
||||||
|
else: |
||||||
|
polygon.extend(poly_points) |
||||||
|
else: |
||||||
|
polygon.extend(poly_points) |
||||||
|
# print("END NEW POLYGON") |
||||||
|
# return LinearRing(polygon) |
||||||
|
return polygon |
||||||
|
|
||||||
|
|
||||||
|
def get_coords_from_shape(face): |
||||||
|
outerpoly = get_polygon_from_subshape(face.OuterWire) |
||||||
|
inner_polys = [] |
||||||
|
for inner_wire in face.SubShapes[1:]: |
||||||
|
tmp = get_polygon_from_subshape(inner_wire) |
||||||
|
inner_polys.append(tmp) |
||||||
|
poly = Polygon(outerpoly, holes=inner_polys) |
||||||
|
return path2DToPathList([poly]) |
||||||
|
|
||||||
|
|
||||||
|
def create_hatch_lines(geoms): |
||||||
|
hatchlines = [] |
||||||
|
for geom in geoms: |
||||||
|
if geom.type() == LayerGeometryType.Hatch: |
||||||
|
# print("Hatch with {} coords".format(len(geom.coords))) |
||||||
|
hatches = np.vstack([geom.coords.reshape(-1, 2, 2)]) |
||||||
|
for line in hatches: |
||||||
|
p0 = line[0] |
||||||
|
p1 = line[1] |
||||||
|
pp = Part.makeLine(App.Vector(p0[0], p0[1],0), App.Vector(p1[0],p1[1],0)) |
||||||
|
hatchlines.append(pp) |
||||||
|
return hatchlines |
||||||
|
|
||||||
|
|
||||||
|
def create_contour_lines(geoms): |
||||||
|
contours = [] |
||||||
|
for geom in geoms: |
||||||
|
if geom.type() == LayerGeometryType.Polygon: |
||||||
|
# print("Contour with {} coords".format(len(geom.coords))) |
||||||
|
coords = rdp.rdp(geom.coords, epsilon=0.3, algo="iter", return_mask=False) |
||||||
|
# print("Simplfied Poly:", len(coords)) |
||||||
|
pp = Part.makePolygon([App.Vector(x,y,0) for (x,y) in coords]) |
||||||
|
contours.append(pp) |
||||||
|
return contours |
||||||
@ -1,6 +0,0 @@ |
|||||||
# module lasercladding |
|
||||||
|
|
||||||
from .commands import * |
|
||||||
from .path import * |
|
||||||
from .job import * |
|
||||||
from .kuka import * |
|
||||||
@ -1,77 +0,0 @@ |
|||||||
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()) |
|
||||||
@ -1,170 +0,0 @@ |
|||||||
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("E6POS startp\n") |
|
||||||
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("BAS (#TOOL,6) ;Initialicing the defaults for Vel and so on \n\n") |
|
||||||
srcfile.write("BAS (#BASE,2) ;Initialicing the defaults for Vel and so on \n\n") |
|
||||||
srcfile.write("PTP {A1 -1.9, A2 -105.76, A3 79.97, A4 178.83, A5 -20.3, A6 -4.37, E1 -90, E2 0}\n") |
|
||||||
srcfile.write(";------------- main part ------------\n") |
|
||||||
srcfile.write("startp=$POS_ACT\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 startp:{} C_VEL; 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 |
|
||||||
@ -1,149 +0,0 @@ |
|||||||
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 |
|
||||||
|
|
||||||
|
|
||||||
@ -1,16 +0,0 @@ |
|||||||
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") |
|
||||||