Path Tooltable scriptability?

Here's the place for discussion related to CAM/CNC and the development of the Path module.
Forum rules
Be nice to others! Respect the FreeCAD code of conduct!
User avatar
onekk
Veteran
Posts: 6144
Joined: Sat Jan 17, 2015 7:48 am
Contact:

Path Tooltable scriptability?

Post by onekk »

I'm trying to make some scripting about Tools.

see for some attempts, also:

https://forum.freecadweb.org/viewtopic. ... 42#p544542


There were many discussion about Tools around this subforum.

But no code around about on creating and managing "Tooltables" (or whatever they are named actually).

From the Gui I could create a proper "Tools" directory in FreeCAD user dirs.

but it seems that there are no exposed Python calls to operate on it.

basic operation that maybe would permit to modify a Tool and save the modification to the Tooltable.

I could maybe interact directly with json structure, but having some methods in Python would be the best way, to make possible some scripts or Macro trhat would translate ToolTables in various formats, into "Path Tooltable".

Any hints, from Path developers?
sliptonic wrote: Tue Nov 16, 2021 3:23 pm Ping
Regards

Carlo D.
GitHub page: https://github.com/onekk/freecad-doc.
- In deep articles on FreeCAD.
- Learning how to model with scripting.
- Various other stuffs.

Blog: https://okkmkblog.wordpress.com/
User avatar
sliptonic
Veteran
Posts: 3457
Joined: Tue Oct 25, 2011 10:46 pm
Location: Columbia, Missouri
Contact:

Re: Path Tooltable scriptability?

Post by sliptonic »

onekk wrote: Thu Nov 18, 2021 5:18 pm I'm trying to make some scripting about Tools.
Sorry, I haven't been following the forum very closely. What goal do you have?
There were many discussion about Tools around this subforum.

But no code around about on creating and managing "Tooltables" (or whatever they are named actually).
The term 'Tooltable' is deprecated (or should be). This is to distinguish the CAM view of tools from the machine controller's view. I think of a 'tooltable' as the specific collection of tools that a machine has access to in the context of a job.

A user, however, might have tools that need to be identified that are not loaded in the machine. These tools need to be organized, and grouped. For that reason, the new Toolbit system uses the term 'Library' to refer to an arbitrary set of tools. It's up to the user to determine what is meant by the particular grouping. It might have a 1:1 relationship with a machine tooltable, or it might just be "all the tools I use for aluminium" or "all the tools I buy from onsrud".
From our (Path) perspective, it doesn't mean anything more than that.
From the Gui I could create a proper "Tools" directory in FreeCAD user dirs.

but it seems that there are no exposed Python calls to operate on it.
As you noted, the persistent structure of tool related data (toolbits and libraries) are just JSON files so you can generate them directly or operate on them as you see fit. The first implementation wasn't done with script-ability in mind.

All the logic for creating them is in PathScripts/PathToolBitLibraryGui.py. Some of this could certainly be refactored out into UI independent (testable) functions. That would be a good thing to do.
spanner888
Posts: 326
Joined: Tue May 28, 2019 10:51 am

Re: Path Tooltable scriptability?

Post by spanner888 »

onekk wrote: Thu Nov 18, 2021 5:18 pm But no code around about on creating and managing "Tooltables" (or whatever they are named actually).
I have been stumbling around this last week and came to similar conclusion, ie current code all gui based.

My rough notes on what I think you need to do to add a new Tool to a Tool Library are:

Create/save ToolBit to file

Code: Select all

    PathScripts.PathToolBit.ToolBit Class 
        >>>def 	saveToFile (self, obj, path, setFile=True)
Then use the TB file to add to named TB Library & save library

Code: Select all

class ToolBitLibrary(object):
    def toolBitNew(self):
uses factory.newTool WHICH is still doing gui stuff!!!!

So I was thinking to find/extract existing relevant code to do non-gui version, but have put it aside for now as really should be doing other things...

However, need to think about what to do if user has the Tool dock open and you edit/add tool to current library, which should then update the tools listed in the Tool dock.

If the Tool Library manager is open, I assume you can't run macro, so no need handle that situation.
User avatar
onekk
Veteran
Posts: 6144
Joined: Sat Jan 17, 2015 7:48 am
Contact:

Re: Path Tooltable scriptability?

Post by onekk »

sliptonic wrote: Thu Nov 18, 2021 8:59 pm
Thanks, what I intended to do, is no more than the ability to create a Tool that go into library.

Let me see I I have catch the mechanics.

we have a Library that essentially is a json file, (Tool Dock Gui)

- I choose a tool that is in this library and this tool is imported in Job in Tools.Group

-- this tool is modified in the Job and the data are relative to "the job only"


A good addition would be as told by Spanner888 to have a mean to use Data from Python and have a save to file option.

I.e maybe this scheme:

ToolLibrary class

create()
addTool()
modTool()
removeTool()
loadFromFile()
saveToFile()

And a mean to load the tool in the Job, for now if I use DefaultTool i could modify the tool data and I'm able to adapt it for my needs.

but to add another Tool to the Tools.Group() I can't see any methods to do, the most useful that come in mind is to have a copy() method so i could code something simple as:

Code: Select all

tl0 = Job.Tools.Group[0]
....
this is the code that adapt default toll data to my needs
....

tl1 = tl0.copy()
....
modify tools data for tl1
....

Job.Tools.Group.append(tl1)

And maybe some methods to manage ToolLibraries, if they stable, I could try to make some script that could resemble the scheme above.

Regards

Carlo D.
GitHub page: https://github.com/onekk/freecad-doc.
- In deep articles on FreeCAD.
- Learning how to model with scripting.
- Various other stuffs.

Blog: https://okkmkblog.wordpress.com/
User avatar
onekk
Veteran
Posts: 6144
Joined: Sat Jan 17, 2015 7:48 am
Contact:

Re: Path Tooltable scriptability?

Post by onekk »

Not much, but a start:

Code: Select all

"""test_library.py

   This code was written as an sample code
   for "FreeCAD Scripting Guide"

   Author: Carlo Dormeletti
   Copyright: 2021
   Licence: CC BY-NC-ND 4.0 IT
"""

import os
import os.path
import glob
import json

import FreeCAD
from FreeCAD import Base, Rotation, Vector

import PathScripts.PathLog as PathLog
import PathScripts.PathPreferences as PathPreferences
import PathScripts.PathToolBit as PathToolBit

DOC_NAME = "Pippo"

DOC = FreeCAD.activeDocument()



def clear_doc():
    """
    Clear the active document deleting all the objects
    """
    for obj in DOC.Objects:
        DOC.removeObject(obj.Name)

if DOC is None:
    FreeCAD.newDocument(DOC_NAME)
    FreeCAD.setActiveDocument(DOC_NAME)
    DOC = FreeCAD.activeDocument()
    VIEW = FreeCAD.Gui.ActiveDocument.ActiveView
else:
    clear_doc()
    VIEW = FreeCAD.Gui.ActiveDocument.ActiveView

def setview():
    """Rearrange View"""
    DOC.recompute()
    VIEW.viewAxometric()
    VIEW.setAxisCross(True)
    VIEW.fitAll()

EPS = 0.10
EPS_C = EPS * -0.5

VZOR = Vector(0,0,0)
RZO = Rotation(0, 0, 0)

def checkWorkingDir():
    # Check existence of a working directory.
    # and check that working directory is writable
    PathLog.track()

    workingdir = os.path.dirname(PathPreferences.lastPathToolLibrary())
    defaultdir = os.path.dirname(PathPreferences.pathDefaultToolsPath())

    msg = 'workingdir: {} defaultdir: {}'.format(workingdir, defaultdir)
    PathLog.debug(msg)

    print(msg)
    dirOK = lambda : workingdir != defaultdir and (os.access(workingdir, os.W_OK))

    if dirOK():
        return True
    else:
        return False


class ToolLibrary(object):
    '''Helper class to access and manage ToolLibraries.'''

    def __init__(self, path=None):
        PathLog.track()
        self.path = ""
        # self.currentLib = ""

    def __libraryLoad(self, path, datamodel):
        PathLog.track(path)
        PathPreferences.setLastFileToolLibrary(path)
        # self.currenLib = path

        with open(path) as fp:
            library = json.load(fp)

        for toolBit in library['tools']:
            try:
                nr = toolBit['nr']
                bit = PathToolBit.findToolBit(toolBit['path'], path)
                if bit:
                    PathLog.track(bit)
                    tool = PathToolBit.Declaration(bit)
                    datamodel.append((nr, tool, bit))
                else:
                    PathLog.error("Could not find tool #{}: {}".format(nr, toolBit['path']))
            except Exception as e:
                msg = "Error loading tool: {} : {}".format(toolBit['path'], e)
                FreeCAD.Console.PrintError(msg)

    def _toolAdd(self, nr, tool, path):

        strShape = os.path.splitext(os.path.basename(tool['shape']))[0]
        # strDiam = tool['parameter']['Diameter']
        tooltip = "{}".format(strShape)

        return [toolNr, toolName, toolShape]

    def newTool(self, datamodel, path):
        """Add a toolbit item to a library.
        """
        PathLog.track()

        try:
            nr = 0

            tool = PathToolBit.Declaration(path)
        except Exception as e:
            PathLog.error(e)

    def findLibraries(self):
        """Find all the fctl files in a location.

        Return a list of filenames with path
        """
        libfiles = []
        PathLog.track()
        path = PathPreferences.lastPathToolLibrary()

        if os.path.isdir(path):
            libFiles = [f for f in glob.glob(path + os.path.sep + '*.fctl')]
            libFiles.sort()
            for libFile in libFiles:
                libfiles.append(libFile)

        PathLog.debug('model rows: {}'.format(len(libfiles)))
        return libfiles

    def libraryOpen(self, lib=""):
        '''
        opens the tools in library
        '''
        model = []
        PathLog.track(lib)

        if lib == "":
            lib = PathPreferences.lastFileToolLibrary()

        if lib == "" or lib is None:
            return model

        print("lib: {}".format(lib))

        if os.path.isfile(lib):  # An individual library is wanted
            self.__libraryLoad(lib, model)

        PathLog.debug('model rows: {}'.format(len(model)))
        return model

    def libraryDump(self, lib=""):
        tools = self.libraryOpen(lib)

        for idx, tool in enumerate(tools):
            msg = ["Index: {}".format(idx)]
            msg.append("ToolNumber: {}".format(tool[0]))

            for mk, mv in tool[1].items():
                if mk not in ('parameter', 'attribute'):
                    msg.append("{}: {}".format(mk, mv))
                else:
                    msg.append("{}:".format(mk))

                    for pk, pv in mv.items():
                     msg.append("-- {}: {}".format(pk, pv))

            out_msg = "\n".join(msg)
            out_msg += "\n-----------------------\n\n"
            FreeCAD.Console.PrintMessage(out_msg)


# CODE start here
workingdir = os.path.dirname(PathPreferences.lastPathToolLibrary())


checkWorkingDir()

libraries = []
tools = []

library = ToolLibrary()

libraries = library.findLibraries()

print("Libraries: {}".format(libraries))

library.libraryDump(libraries[1])


print("--- done ---")
As suggested by @sliptonic I've done some work, copying and modifying some methods from /Path/PathScripts/PathToolBitLibraryGui.py as found in 0.20.26202 sources that I have on my hd.

it is more a test code to see how thing works, but just in case someone will find it useful, I put here.

Most of the code is simply the original code copied and slightly modified so maintaining will be more plain as original methods names are nearly identical.

Some description, it nedds a custom Tool Library in the User directory, like one of those created with the Gui ToolLibrary.

For now only libraryDump is added the code will output some data printing them in the Report Window, as a FreeCAD.Console.Message.

some little explanation:

in the user directory a Tools directory is created by the Gui Tool, there are three sub directories
  1. Bit where the ToolBit definition reside
  2. Library where are the Library files (Tool Collections)
  3. Shape if you don't use custom shapes this is empty in current implementation
a "Tool Collection" file is a json file ending with .fctl that simply contains the toll number and a reference to the corresponding Bit file

Code: Select all

{
  "tools": [
    {
      "nr": 1,
      "path": "5mm_Endmill.fctb"
    },
    {
      "nr": 2,
      "path": "5mm_Drill.fctb"
    },
    {
      "nr": 3,
      "path": "6mm_Ball_End.fctb"
    },
    {
      "nr": 4,
      "path": "6mm_Bullnose.fctb"
    },
    {
      "nr": 5,
      "path": "60degree_Vbit.fctb"
    }
  ],
  "version": 1
}
This is the content of the Bit file:

Code: Select all

{
  "version": 2,
  "name": "5mm Endmill",
  "shape": "endmill.fcstd",
  "parameter": {
    "CuttingEdgeHeight": "30.0000 mm",
    "Diameter": "5.0000 mm",
    "Length": "50.0000 mm",
    "ShankDiameter": "3.0000 mm"
  },
  "attribute": {}
}

So now it all to write some helpers methods that create appropriate files under Bit subdir and add the appropriate stanza to the "Tool Collection"

Regards

Carlo D.
GitHub page: https://github.com/onekk/freecad-doc.
- In deep articles on FreeCAD.
- Learning how to model with scripting.
- Various other stuffs.

Blog: https://okkmkblog.wordpress.com/
spanner888
Posts: 326
Joined: Tue May 28, 2019 10:51 am

Re: Path Tooltable scriptability?

Post by spanner888 »

Hi Carlo

The libarary code above works very nicely - thanks!

onekk wrote: Fri Nov 19, 2021 10:06 am And a mean to load the tool in the Job, for now if I use DefaultTool i could modify the tool data and I'm able to adapt it for my needs.
I have been using below as part of code that creates ToolBits and then uses those to add Toolcontrollers into a Path-Job. Your code above will provide a way to get desired existing Tool from a specified library, then add to Job.

Not shown below is how to get Job object, but all that is already code you already published previously, that I already borrowed!

Code: Select all

# Based on FreeCAD TestPathToolController.py
def createTool(name='t1', diameter=1.75, shape=None):
    if PathPreferences.toolsUseLegacyTools():
        return Path.Tool(name=name, diameter=diameter)
    attrs = {'shape': pathPrefs.GetString("LastPathToolShape") + '/' + shape + '.fcstd', 'name': name, 'parameter': {'Diameter': diameter}, 'attribute': []}
    return PathToolBit.Factory.CreateFromAttrs(attrs, name)

# Based on FreeCAD PathJob:
def addToolController(job, tc):  
    group = job.Tools.Group
    if tc.Name not in [str(t.Name) for t in group]:
        job.Tools.addObject(tc)
imm
Posts: 250
Joined: Wed Nov 03, 2021 1:00 pm

Re: Path Tooltable scriptability?

Post by imm »

I am looking at this code and thinking of giving a run at it with a simple one-shot csv file for the tool-library...(shapefiles excluded of course)
Not shown below is how to get Job object, but all that is already code you already published previously
@spanner888 if you have a link to the code snippet mentioned can you post it?

I am interested.
spanner888
Posts: 326
Joined: Tue May 28, 2019 10:51 am

Re: Path Tooltable scriptability?

Post by spanner888 »

imm wrote: Fri Dec 03, 2021 2:16 pm @spanner888 if you have a link to the code snippet mentioned can you post it?

Code: Select all

# Now functions related to Tools & ToolControllers
def initJob(jobName, thisShape, template = None):
    # job = PathJob.Create(jobName, [thisShape], template)
    job = PathJob.Create('Job', [thisShape], template)
    # job = PathJobGui.Create([thisShape], template)
    job.ViewObject.Proxy = PathJobGui.ViewProvider(job.ViewObject)
    job.Label = jobName
    
    return (job)

# Based on FreeCAD TestPathToolController.py
def createTool(name='t1', diameter=1.75, shape=None):
    if PathPreferences.toolsUseLegacyTools():
        return Path.Tool(name=name, diameter=diameter)
    # attrs = {'shape': None, 'name': name, 'parameter': {'Diameter': diameter}, 'attribute': []}
    attrs = {'shape': pathPrefs.GetString("LastPathToolShape") + '/' + shape + '.fcstd', 'name': name, 'parameter': {'Diameter': diameter}, 'attribute': []}
    # tool = PathToolBit.Factory.CreateFromAttrs(attrs, name)
    # toolShapePath = pathPrefs.GetString("LastPathToolShape") + '/' + shape + '.fcstd'
    # print(toolShapePath)
    # tool.BitShape = toolShapePath
    return PathToolBit.Factory.CreateFromAttrs(attrs, name)

# Based on FreeCAD PathJob:
def addToolController(job, tc):  #<<<??skip pass in & create TC here as well??
    group = job.Tools.Group
    # PathLog.debug("addToolController(%s): %s" % (tc.Label, [t.Label for t in group]))
    if tc.Name not in [str(t.Name) for t in group]:
        # tc.setExpression('VertRapid', "%s.%s" % (job.setupSheet.expressionReference(), PathSetupSheet.Template.VertRapid))
        # tc.setExpression('HorizRapid', "%s.%s" % (job .setupSheet.expressionReference(), PathSetupSheet.Template.HorizRapid))
        job.Tools.addObject(tc)


# so for auto tool creation - setup to create RANGE of tools, based on say changing dia...and auto adapt F&S for each
# ...so also pass in base f&s as well as other tool prop
# TODO add mac feed & rpm checks as well as proper F&S calcs or lookups
# Imperial feed/rapid example '3 in/s' & metric '10 mm/s'

# TODO FIXME: tc double click DOES open gui editor panel
# ...toolbit d'click DOES NOT BUT  error is 
# if name.startswith('.'):<class 'AttributeError'>: 'NoneType' object has no attribute 'startswith'
# above error while trying to open edit panel??
def createRangeToolControllers(toolNumberStart, toolCount, toolShape, 
                                    diaStart, diaInc, 
                                    vRapid, vFeedStart, vFeedInc, 
                                    hRapid, hFeedStart, hFeedInc, 
                                    spRpmStart, spRpmInc, spDir ='Forward'):
    """ Create & add to the Job a range of ToolController (with rapids, feeds & speeds) & assoc ToolBits & properties

        Tool diameters, feeds and speed are incremented as specified for each tool.
        In general increasing tool diameter would require decreasing feeds or speeds or vice verse.
        So use positive increment for Tool dia and negative for feeds & speeds or vice versa."""                                                        

    for toolIndex in range (toolCount):
        
        # create Tool start dia = 2.5, incrementing 1.5mmm for example
        toolDia  = diaStart + toolIndex*diaInc
        # Naming this way to avoid trailing numbers being auto renamed by FC
        t  = createTool("T%04d_%s" %(100*toolDia, toolShape), toolDia, toolShape)
        print("created toolbit: T%04d_%s" %(100*toolDia, toolShape))
        
        # Now for ToolController
        tc = PathToolController.Create("TC%02d_" % toolIndex, t)
        # tcProps = PathToolController.ToolControllerTemplate
        tc.Label = "TC%04d_%s" %(100*toolDia, toolShape)
        tc.ToolNumber = toolNumberStart + toolIndex
        
        tc.VertRapid = vRapid/60
        # tc.VertRapid = round(tc.VertRapid, 2)
        
        tc.VertFeed =  (vFeedStart + toolIndex * vFeedInc)/60
        # tc.VertFeed = round(tc.VertFeed, 2)
        
        tc.HorizRapid = hRapid/60
        # tc.HorizRapid = round(tc.HorizRapid, 2)
        
        tc.HorizFeed = (hFeedStart + toolIndex * hFeedInc)/60
        # tc.HorizFeed = round(tc.HorizFeed, 2)
        
        tc.SpindleSpeed = spRpmStart + toolIndex * spRpmInc
        tc.SpindleDir = spDir
        addToolController(job, tc)
If the above is not all you need, I can (re-)publish on git the full code...it got a bit buried in my git repo after a pull request was only partly accepted and I keep doing stupid things in git...
imm
Posts: 250
Joined: Wed Nov 03, 2021 1:00 pm

Re: Path Tooltable scriptability?

Post by imm »

I was actually looking for code that would provide an object linked to the existing 'Job' object within the current document.....Even if I am looking to 'Init' a Tool Collection/Library from a csv source I would want to associate that with the current Job.

At least that is my current understanding and my intent.

By creating a functional process to load a Group of tools in a user-defined document that can be associated with an existing Job ....or Job + Process .... I am hoping to provide users an alternate means to maintain their tooling information while the GUI interface is in a 'fluid' state.

That being said it will also be applicable to those looking to build an entire process from scratch using scripting.
spanner888
Posts: 326
Joined: Tue May 28, 2019 10:51 am

Re: Path Tooltable scriptability?

Post by spanner888 »

imm wrote: Sat Dec 04, 2021 11:56 am I was actually looking for code that would provide an object linked to the existing 'Job' object within the current document..
Below is from FeedsAndSpeeds addon:

Code: Select all

    def load_tools(self):
        jobs = FreeCAD.ActiveDocument.findObjects("Path::FeaturePython", "Job.*")
        # self.form.toolController_CB.addItem('None')
        for job in jobs:
            for idx, tc in enumerate(job.Tools.Group):
                self.form.toolController_CB.addItem(tc.Label)
Post Reply