Moved Path ToolBit and Controller into Path.Tools module

This commit is contained in:
Markus Lampert
2022-08-10 20:39:53 -07:00
parent 1d27fb00ec
commit 20d2d4c8d0
27 changed files with 205 additions and 56 deletions

View File

@@ -0,0 +1,123 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2022 sliptonic <shopinthewoods@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
import FreeCAD
import Path
import PathScripts.PathGeom as PathGeom
import PathScripts.PathLanguage as PathLanguage
import PathScripts.Path.Log as Path.Log
import math
class Kink (object):
'''A Kink represents the angle at which two moves connect.
A positive kink angle represents a move to the left, and a negative angle represents a move to the right.'''
def __init__(self, m0, m1):
if m1 is None:
m1 = m0[1]
m0 = m0[0]
self.m0 = m0
self.m1 = m1
self.t0 = m0.anglesOfTangents()[1]
self.t1 = m1.anglesOfTangents()[0]
def deflection(self):
'''deflection() ... returns the tangential difference of the two edges at their intersection'''
return PathGeom.normalizeAngle(self.t1 - self.t0)
def normAngle(self):
'''normAngle() ... returns the angle opposite between the two tangents'''
# The normal angle is perpendicular to the "average tangent" of the kink. The question
# is into which direction to turn. One lies in the center between the two edges and the
# other is opposite to that. As it turns out, the magnitude of the tangents tell it all.
if self.t0 > self.t1:
return PathGeom.normalizeAngle((self.t0 + self.t1 + math.pi) / 2)
return PathGeom.normalizeAngle((self.t0 + self.t1 - math.pi) / 2)
def position(self):
'''position() ... position of the edge's intersection'''
return self.m0.positionEnd()
def x(self):
return self.position().x
def y(self):
return self.position().y
def __repr__(self):
return f"({self.x():.4f}, {self.y():.4f})[t0={180*self.t0/math.pi:.2f}, t1={180*self.t1/math.pi:.2f}, deflection={180*self.deflection()/math.pi:.2f}, normAngle={180*self.normAngle()/math.pi:.2f}]"
def createKinks(maneuver):
k = []
moves = maneuver.getMoves()
if moves:
move0 = moves[0]
prev = move0
for m in moves[1:]:
k.append(Kink(prev, m))
prev = m
if PathGeom.pointsCoincide(move0.positionBegin(), prev.positionEnd()):
k.append(Kink(prev, move0))
return k
def findDogboneKinks(maneuver, threshold):
'''findDogboneKinks(maneuver, threshold) ... return all kinks fitting the criteria.
A positive threshold angle returns all kinks on the right side, and a negative all kinks on the left side'''
if threshold > 0:
return [k for k in createKinks(maneuver) if k.deflection() > threshold]
if threshold < 0:
return [k for k in createKinks(maneuver) if k.deflection() < threshold]
# you asked for it ...
return createKinks(maneuver)
class Bone (object):
'''A Bone holds all the information of a bone and the kink it is attached to'''
def __init__(self, kink, angle, instr=None):
self.kink = kink
self.angle = angle
self.instr = [] if instr is None else instr
def addInstruction(self, instr):
self.instr.append(instr)
def kink_to_path(kink, g0=False):
return Path.Path([PathLanguage.instruction_to_command(instr) for instr in [kink.m0, kink.m1]])
def bone_to_path(bone, g0=False):
kink = bone.kink
cmds = []
if g0 and not PathGeom.pointsCoincide(kink.m0.positionBegin(), FreeCAD.Vector(0, 0, 0)):
pos = kink.m0.positionBegin()
param = {}
if not PathGeom.isRoughly(pos.x, 0):
param['X'] = pos.x
if not PathGeom.isRoughly(pos.y, 0):
param['Y'] = pos.y
cmds.append(Path.Command('G0', param))
for instr in [kink.m0, bone.instr[0], bone.instr[1], kink.m1]:
cmds.append(PathLanguage.instruction_to_command(instr))
return Path.Path(cmds)

View File

@@ -39,6 +39,9 @@ def Startup():
if not Processed:
Path.Log.debug("Initializing PathGui")
from Path.Op.Gui import Adaptive
from Path.Post import Command
from Path.Tools import Controller
from Path.Tools.Gui import Controller
from PathScripts import PathArray
from PathScripts import PathComment
from PathScripts import PathCustomGui
@@ -60,7 +63,6 @@ def Startup():
from PathScripts import PathMillFaceGui
from PathScripts import PathPocketGui
from PathScripts import PathPocketShapeGui
from Path.Post import Command
from PathScripts import PathProbeGui
from PathScripts import PathProfileGui
from PathScripts import PathPropertyBagGui
@@ -71,8 +73,6 @@ def Startup():
from PathScripts import PathSlotGui
from PathScripts import PathStop
from PathScripts import PathThreadMillingGui
from PathScripts import PathToolController
from PathScripts import PathToolControllerGui
from PathScripts import PathToolLibraryEditor
from PathScripts import PathToolLibraryManager
from PathScripts import PathUtilsGui

View File

@@ -24,11 +24,11 @@ from PySide import QtCore
from PySide.QtCore import QT_TRANSLATE_NOOP
import FreeCAD
import Path
import Path.Post.Processor as PostProcessor
from Path.Post.Processor import PostProcessor
import Path.Tools.Controller as PathToolController
import PathScripts.PathPreferences as PathPreferences
import PathScripts.PathSetupSheet as PathSetupSheet
import PathScripts.PathStock as PathStock
import PathScripts.PathToolController as PathToolController
import PathScripts.PathUtil as PathUtil
import json
import time

View File

@@ -28,6 +28,8 @@ from pivy import coin
import FreeCAD
import FreeCADGui
import Path
import Path.Tools.Gui.Bit as PathToolBitGui
import Path.Tools.Gui.Controller as PathToolControllerGui
import PathScripts.PathGeom as PathGeom
import PathScripts.PathGuiInit as PathGuiInit
import PathScripts.PathJob as PathJob
@@ -36,8 +38,6 @@ import PathScripts.PathJobDlg as PathJobDlg
import PathScripts.PathPreferences as PathPreferences
import PathScripts.PathSetupSheetGui as PathSetupSheetGui
import PathScripts.PathStock as PathStock
import PathScripts.PathToolBitGui as PathToolBitGui
import PathScripts.PathToolControllerGui as PathToolControllerGui
import PathScripts.PathToolLibraryEditor as PathToolLibraryEditor
import PathScripts.PathUtil as PathUtil
import PathScripts.PathUtils as PathUtils

View File

@@ -1,506 +0,0 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2019 sliptonic <shopinthewoods@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
import FreeCAD
import Path
import PathScripts.PathPreferences as PathPreferences
import PathScripts.PathPropertyBag as PathPropertyBag
import PathScripts.PathUtil as PathUtil
import json
import os
import zipfile
from PySide.QtCore import QT_TRANSLATE_NOOP
# lazily loaded modules
from lazy_loader.lazy_loader import LazyLoader
Part = LazyLoader("Part", globals(), "Part")
__title__ = "Tool bits."
__author__ = "sliptonic (Brad Collette)"
__url__ = "https://www.freecadweb.org"
__doc__ = "Class to deal with and represent a tool bit."
PropertyGroupShape = "Shape"
_DebugFindTool = False
if False:
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
else:
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
def _findToolFile(name, containerFile, typ):
Path.Log.track(name)
if os.path.exists(name): # absolute reference
return name
if containerFile:
rootPath = os.path.dirname(os.path.dirname(containerFile))
paths = [os.path.join(rootPath, typ)]
else:
paths = []
paths.extend(PathPreferences.searchPathsTool(typ))
def _findFile(path, name):
Path.Log.track(path, name)
fullPath = os.path.join(path, name)
if os.path.exists(fullPath):
return (True, fullPath)
for root, ds, fs in os.walk(path):
for d in ds:
found, fullPath = _findFile(d, name)
if found:
return (True, fullPath)
return (False, None)
for p in paths:
found, path = _findFile(p, name)
if found:
return path
return None
def findToolShape(name, path=None):
"""findToolShape(name, path) ... search for name, if relative path look in path"""
Path.Log.track(name, path)
return _findToolFile(name, path, "Shape")
def findToolBit(name, path=None):
"""findToolBit(name, path) ... search for name, if relative path look in path"""
Path.Log.track(name, path)
if name.endswith(".fctb"):
return _findToolFile(name, path, "Bit")
return _findToolFile("{}.fctb".format(name), path, "Bit")
# Only used in ToolBit unit test module: TestPathToolBit.py
def findToolLibrary(name, path=None):
"""findToolLibrary(name, path) ... search for name, if relative path look in path"""
Path.Log.track(name, path)
if name.endswith(".fctl"):
return _findToolFile(name, path, "Library")
return _findToolFile("{}.fctl".format(name), path, "Library")
def _findRelativePath(path, typ):
Path.Log.track(path, typ)
relative = path
for p in PathPreferences.searchPathsTool(typ):
if path.startswith(p):
p = path[len(p) :]
if os.path.sep == p[0]:
p = p[1:]
if len(p) < len(relative):
relative = p
return relative
# Unused due to bug fix related to relative paths
"""
def findRelativePathShape(path):
return _findRelativePath(path, 'Shape')
def findRelativePathTool(path):
return _findRelativePath(path, 'Bit')
"""
def findRelativePathLibrary(path):
return _findRelativePath(path, "Library")
class ToolBit(object):
def __init__(self, obj, shapeFile, path=None):
Path.Log.track(obj.Label, shapeFile, path)
self.obj = obj
obj.addProperty(
"App::PropertyFile",
"BitShape",
"Base",
QT_TRANSLATE_NOOP("App::Property", "Shape for bit shape"),
)
obj.addProperty(
"App::PropertyLink",
"BitBody",
"Base",
QT_TRANSLATE_NOOP(
"App::Property", "The parametrized body representing the tool bit"
),
)
obj.addProperty(
"App::PropertyFile",
"File",
"Base",
QT_TRANSLATE_NOOP("App::Property", "The file of the tool"),
)
obj.addProperty(
"App::PropertyString",
"ShapeName",
"Base",
QT_TRANSLATE_NOOP("App::Property", "The name of the shape file"),
)
obj.addProperty(
"App::PropertyStringList",
"BitPropertyNames",
"Base",
QT_TRANSLATE_NOOP(
"App::Property", "List of all properties inherited from the bit"
),
)
if path:
obj.File = path
if shapeFile is None:
obj.BitShape = "endmill.fcstd"
self._setupBitShape(obj)
self.unloadBitBody(obj)
else:
obj.BitShape = shapeFile
self._setupBitShape(obj)
self.onDocumentRestored(obj)
def __getstate__(self):
return None
def __setstate__(self, state):
for obj in FreeCAD.ActiveDocument.Objects:
if hasattr(obj, "Proxy") and obj.Proxy == self:
self.obj = obj
break
return None
def onDocumentRestored(self, obj):
# when files are shared it is essential to be able to change/set the shape file,
# otherwise the file is hard to use
# obj.setEditorMode('BitShape', 1)
obj.setEditorMode("BitBody", 2)
obj.setEditorMode("File", 1)
obj.setEditorMode("Shape", 2)
if not hasattr(obj, "BitPropertyNames"):
obj.addProperty(
"App::PropertyStringList",
"BitPropertyNames",
"Base",
QT_TRANSLATE_NOOP(
"App::Property", "List of all properties inherited from the bit"
),
)
propNames = []
for prop in obj.PropertiesList:
if obj.getGroupOfProperty(prop) == "Bit":
val = obj.getPropertyByName(prop)
typ = obj.getTypeIdOfProperty(prop)
dsc = obj.getDocumentationOfProperty(prop)
obj.removeProperty(prop)
obj.addProperty(typ, prop, PropertyGroupShape, dsc)
PathUtil.setProperty(obj, prop, val)
propNames.append(prop)
elif obj.getGroupOfProperty(prop) == "Attribute":
propNames.append(prop)
obj.BitPropertyNames = propNames
obj.setEditorMode("BitPropertyNames", 2)
for prop in obj.BitPropertyNames:
if obj.getGroupOfProperty(prop) == PropertyGroupShape:
# properties in the Shape group can only be modified while the actual
# shape is loaded, so we have to disable direct property editing
obj.setEditorMode(prop, 1)
else:
# all other custom properties can and should be edited directly in the
# property editor widget, not much value in re-implementing that
obj.setEditorMode(prop, 0)
def onChanged(self, obj, prop):
Path.Log.track(obj.Label, prop)
if prop == "BitShape" and "Restore" not in obj.State:
self._setupBitShape(obj)
def onDelete(self, obj, arg2=None):
Path.Log.track(obj.Label)
self.unloadBitBody(obj)
obj.Document.removeObject(obj.Name)
def _updateBitShape(self, obj, properties=None):
if obj.BitBody is not None:
for attributes in [
o
for o in obj.BitBody.Group
if hasattr(o, "Proxy") and hasattr(o.Proxy, "getCustomProperties")
]:
for prop in attributes.Proxy.getCustomProperties():
# the property might not exist in our local object (new attribute in shape)
# for such attributes we just keep the default
if hasattr(obj, prop):
setattr(attributes, prop, obj.getPropertyByName(prop))
else:
# if the template shape has a new attribute defined we should add that
# to the local object
self._setupProperty(obj, prop, attributes)
propNames = obj.BitPropertyNames
propNames.append(prop)
obj.BitPropertyNames = propNames
self._copyBitShape(obj)
def _copyBitShape(self, obj):
obj.Document.recompute()
if obj.BitBody and obj.BitBody.Shape:
obj.Shape = obj.BitBody.Shape
else:
obj.Shape = Part.Shape()
def _loadBitBody(self, obj, path=None):
Path.Log.track(obj.Label, path)
p = path if path else obj.BitShape
docOpened = False
doc = None
for d in FreeCAD.listDocuments():
if FreeCAD.getDocument(d).FileName == p:
doc = FreeCAD.getDocument(d)
break
if doc is None:
p = findToolShape(p, path if path else obj.File)
if p is None:
raise FileNotFoundError
if not path and p != obj.BitShape:
obj.BitShape = p
Path.Log.debug("ToolBit {} using shape file: {}".format(obj.Label, p))
doc = FreeCAD.openDocument(p, True)
obj.ShapeName = doc.Name
docOpened = True
else:
Path.Log.debug("ToolBit {} already open: {}".format(obj.Label, doc))
return (doc, docOpened)
def _removeBitBody(self, obj):
if obj.BitBody:
obj.BitBody.removeObjectsFromDocument()
obj.Document.removeObject(obj.BitBody.Name)
obj.BitBody = None
def _deleteBitSetup(self, obj):
Path.Log.track(obj.Label)
self._removeBitBody(obj)
self._copyBitShape(obj)
for prop in obj.BitPropertyNames:
obj.removeProperty(prop)
def loadBitBody(self, obj, force=False):
if force or not obj.BitBody:
activeDoc = FreeCAD.ActiveDocument
if force:
self._removeBitBody(obj)
(doc, opened) = self._loadBitBody(obj)
obj.BitBody = obj.Document.copyObject(doc.RootObjects[0], True)
if opened:
FreeCAD.setActiveDocument(activeDoc.Name)
FreeCAD.closeDocument(doc.Name)
self._updateBitShape(obj)
def unloadBitBody(self, obj):
self._removeBitBody(obj)
def _setupProperty(self, obj, prop, orig):
# extract property parameters and values so it can be copied
val = orig.getPropertyByName(prop)
typ = orig.getTypeIdOfProperty(prop)
grp = orig.getGroupOfProperty(prop)
dsc = orig.getDocumentationOfProperty(prop)
obj.addProperty(typ, prop, grp, dsc)
if "App::PropertyEnumeration" == typ:
setattr(obj, prop, orig.getEnumerationsOfProperty(prop))
obj.setEditorMode(prop, 1)
PathUtil.setProperty(obj, prop, val)
def _setupBitShape(self, obj, path=None):
Path.Log.track(obj.Label)
activeDoc = FreeCAD.ActiveDocument
(doc, docOpened) = self._loadBitBody(obj, path)
obj.Label = doc.RootObjects[0].Label
self._deleteBitSetup(obj)
bitBody = obj.Document.copyObject(doc.RootObjects[0], True)
docName = doc.Name
if docOpened:
FreeCAD.setActiveDocument(activeDoc.Name)
FreeCAD.closeDocument(doc.Name)
if bitBody.ViewObject:
bitBody.ViewObject.Visibility = False
Path.Log.debug(
"bitBody.{} ({}): {}".format(bitBody.Label, bitBody.Name, type(bitBody))
)
propNames = []
for attributes in [
o for o in bitBody.Group if PathPropertyBag.IsPropertyBag(o)
]:
Path.Log.debug("Process properties from {}".format(attributes.Label))
for prop in attributes.Proxy.getCustomProperties():
self._setupProperty(obj, prop, attributes)
propNames.append(prop)
if not propNames:
Path.Log.error(
"Did not find a PropertyBag in {} - not a ToolBit shape?".format(
docName
)
)
# has to happen last because it could trigger op.execute evaluations
obj.BitPropertyNames = propNames
obj.BitBody = bitBody
self._copyBitShape(obj)
def toolShapeProperties(self, obj):
"""toolShapeProperties(obj) ... return all properties defining it's shape"""
return sorted(
[
prop
for prop in obj.BitPropertyNames
if obj.getGroupOfProperty(prop) == PropertyGroupShape
]
)
def toolAdditionalProperties(self, obj):
"""toolShapeProperties(obj) ... return all properties unrelated to it's shape"""
return sorted(
[
prop
for prop in obj.BitPropertyNames
if obj.getGroupOfProperty(prop) != PropertyGroupShape
]
)
def toolGroupsAndProperties(self, obj, includeShape=True):
"""toolGroupsAndProperties(obj) ... returns a dictionary of group names with a list of property names."""
category = {}
for prop in obj.BitPropertyNames:
group = obj.getGroupOfProperty(prop)
if includeShape or group != PropertyGroupShape:
properties = category.get(group, [])
properties.append(prop)
category[group] = properties
return category
def getBitThumbnail(self, obj):
if obj.BitShape:
path = findToolShape(obj.BitShape)
if path:
with open(path, "rb") as fd:
try:
zf = zipfile.ZipFile(fd)
pf = zf.open("thumbnails/Thumbnail.png", "r")
data = pf.read()
pf.close()
return data
except KeyError:
pass
return None
def saveToFile(self, obj, path, setFile=True):
Path.Log.track(path)
try:
with open(path, "w") as fp:
json.dump(self.templateAttrs(obj), fp, indent=" ")
if setFile:
obj.File = path
return True
except (OSError, IOError) as e:
Path.Log.error(
"Could not save tool {} to {} ({})".format(obj.Label, path, e)
)
raise
def templateAttrs(self, obj):
attrs = {}
attrs["version"] = 2 # Path.Tool is version 1
attrs["name"] = obj.Label
if PathPreferences.toolsStoreAbsolutePaths():
attrs["shape"] = obj.BitShape
else:
# attrs['shape'] = findRelativePathShape(obj.BitShape)
# Extract the name of the shape file
__, filShp = os.path.split(
obj.BitShape
) # __ is an ignored placeholder acknowledged by LGTM
attrs["shape"] = str(filShp)
params = {}
for name in obj.BitPropertyNames:
params[name] = PathUtil.getPropertyValueString(obj, name)
attrs["parameter"] = params
params = {}
attrs["attribute"] = params
return attrs
def Declaration(path):
Path.Log.track(path)
with open(path, "r") as fp:
return json.load(fp)
class ToolBitFactory(object):
def CreateFromAttrs(self, attrs, name="ToolBit", path=None):
Path.Log.track(attrs, path)
obj = Factory.Create(name, attrs["shape"], path)
obj.Label = attrs["name"]
params = attrs["parameter"]
for prop in params:
PathUtil.setProperty(obj, prop, params[prop])
obj.Proxy._updateBitShape(obj)
obj.Proxy.unloadBitBody(obj)
return obj
def CreateFrom(self, path, name="ToolBit"):
Path.Log.track(name, path)
if not os.path.isfile(path):
raise FileNotFoundError(f"{path} not found")
try:
data = Declaration(path)
bit = Factory.CreateFromAttrs(data, name, path)
return bit
except (OSError, IOError) as e:
Path.Log.error("%s not a valid tool file (%s)" % (path, e))
raise
def Create(self, name="ToolBit", shapeFile=None, path=None):
Path.Log.track(name, shapeFile, path)
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", name)
obj.Proxy = ToolBit(obj, shapeFile, path)
return obj
Factory = ToolBitFactory()

View File

@@ -1,175 +0,0 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2019 sliptonic <shopinthewoods@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
import FreeCAD
import FreeCADGui
import Path
import PathScripts
import os
from PySide import QtCore
from PySide.QtCore import QT_TRANSLATE_NOOP
if False:
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
else:
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
class CommandToolBitCreate:
"""
Command used to create a new Tool.
"""
def __init__(self):
pass
def GetResources(self):
return {
"Pixmap": "Path_ToolBit",
"MenuText": QT_TRANSLATE_NOOP("Path_ToolBitCreate", "Create Tool"),
"ToolTip": QT_TRANSLATE_NOOP(
"Path_ToolBitCreate", "Creates a new ToolBit object"
),
}
def IsActive(self):
return FreeCAD.ActiveDocument is not None
def Activated(self):
obj = PathScripts.PathToolBit.Factory.Create()
obj.ViewObject.Proxy.setCreate(obj.ViewObject)
class CommandToolBitSave:
"""
Command used to save an existing Tool to a file.
"""
def __init__(self, saveAs):
self.saveAs = saveAs
def GetResources(self):
if self.saveAs:
menuTxt = QT_TRANSLATE_NOOP("Path_ToolBitSaveAs", "Save Tool as...")
else:
menuTxt = QT_TRANSLATE_NOOP("Path_ToolBitSave", "Save Tool")
return {
"Pixmap": "Path_ToolBit",
"MenuText": menuTxt,
"ToolTip": QT_TRANSLATE_NOOP(
"Path_ToolBitSave", "Save an existing ToolBit object to a file"
),
}
def selectedTool(self):
sel = FreeCADGui.Selection.getSelectionEx()
if 1 == len(sel) and isinstance(
sel[0].Object.Proxy, PathScripts.PathToolBit.ToolBit
):
return sel[0].Object
return None
def IsActive(self):
tool = self.selectedTool()
if tool:
if tool.File:
return True
return self.saveAs
return False
def Activated(self):
from PySide import QtGui
tool = self.selectedTool()
if tool:
path = None
if not tool.File or self.saveAs:
if tool.File:
fname = tool.File
else:
fname = os.path.join(
PathScripts.PathPreferences.lastPathToolBit(),
tool.Label + ".fctb",
)
foo = QtGui.QFileDialog.getSaveFileName(
QtGui.QApplication.activeWindow(), "Tool", fname, "*.fctb"
)
if foo:
path = foo[0]
else:
path = tool.File
if path:
if not path.endswith(".fctb"):
path += ".fctb"
tool.Proxy.saveToFile(tool, path)
PathScripts.PathPreferences.setLastPathToolBit(os.path.dirname(path))
class CommandToolBitLoad:
"""
Command used to load an existing Tool from a file into the current document.
"""
def __init__(self):
pass
def GetResources(self):
return {
"Pixmap": "Path_ToolBit",
"MenuText": QT_TRANSLATE_NOOP("Path_ToolBitLoad", "Load Tool"),
"ToolTip": QT_TRANSLATE_NOOP(
"Path_ToolBitLoad", "Load an existing ToolBit object from a file"
),
}
def selectedTool(self):
sel = FreeCADGui.Selection.getSelectionEx()
if 1 == len(sel) and isinstance(
sel[0].Object.Proxy, PathScripts.PathToolBit.ToolBit
):
return sel[0].Object
return None
def IsActive(self):
return FreeCAD.ActiveDocument is not None
def Activated(self):
if PathScripts.PathToolBitGui.LoadTools():
FreeCAD.ActiveDocument.recompute()
if FreeCAD.GuiUp:
FreeCADGui.addCommand("Path_ToolBitCreate", CommandToolBitCreate())
FreeCADGui.addCommand("Path_ToolBitLoad", CommandToolBitLoad())
FreeCADGui.addCommand("Path_ToolBitSave", CommandToolBitSave(False))
FreeCADGui.addCommand("Path_ToolBitSaveAs", CommandToolBitSave(True))
CommandList = [
"Path_ToolBitCreate",
"Path_ToolBitLoad",
"Path_ToolBitSave",
"Path_ToolBitSaveAs",
]
FreeCAD.Console.PrintLog("Loading PathToolBitCmd... done\n")

View File

@@ -1,280 +0,0 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2019 sliptonic <shopinthewoods@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
from PySide import QtCore, QtGui
import FreeCADGui
import Path
import PathScripts.PathGui as PathGui
import PathScripts.PathPreferences as PathPreferences
import PathScripts.PathPropertyEditor as PathPropertyEditor
import PathScripts.PathUtil as PathUtil
import os
import re
if False:
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
else:
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
class _Delegate(QtGui.QStyledItemDelegate):
"""Handles the creation of an appropriate editing widget for a given property."""
ObjectRole = QtCore.Qt.UserRole + 1
PropertyRole = QtCore.Qt.UserRole + 2
EditorRole = QtCore.Qt.UserRole + 3
def createEditor(self, parent, option, index):
editor = index.data(self.EditorRole)
if editor is None:
obj = index.data(self.ObjectRole)
prp = index.data(self.PropertyRole)
editor = PathPropertyEditor.Editor(obj, prp)
index.model().setData(index, editor, self.EditorRole)
return editor.widget(parent)
def setEditorData(self, widget, index):
# called to update the widget with the current data
index.data(self.EditorRole).setEditorData(widget)
def setModelData(self, widget, model, index):
# called to update the model with the data from the widget
editor = index.data(self.EditorRole)
editor.setModelData(widget)
index.model().setData(
index,
PathUtil.getPropertyValueString(editor.obj, editor.prop),
QtCore.Qt.DisplayRole,
)
class ToolBitEditor(object):
"""UI and controller for editing a ToolBit.
The controller embeds the UI to the parentWidget which has to have a
layout attached to it.
"""
def __init__(self, tool, parentWidget=None, loadBitBody=True):
Path.Log.track()
self.form = FreeCADGui.PySideUic.loadUi(":/panels/ToolBitEditor.ui")
if parentWidget:
self.form.setParent(parentWidget)
parentWidget.layout().addWidget(self.form)
self.tool = tool
self.loadbitbody = loadBitBody
if not tool.BitShape:
self.tool.BitShape = "endmill.fcstd"
if self.loadbitbody:
self.tool.Proxy.loadBitBody(self.tool)
# remove example widgets
layout = self.form.bitParams.layout()
for i in range(layout.rowCount() - 1, -1, -1):
layout.removeRow(i)
# used to track property widgets and editors
self.widgets = []
self.setupTool(self.tool)
self.setupAttributes(self.tool)
def setupTool(self, tool):
Path.Log.track()
# Can't delete and add fields to the form because of dangling references in case of
# a focus change. see https://forum.freecadweb.org/viewtopic.php?f=10&t=52246#p458583
# Instead we keep widgets once created and use them for new properties, and hide all
# which aren't being needed anymore.
def labelText(name):
return re.sub("([A-Z][a-z]+)", r" \1", re.sub("([A-Z]+)", r" \1", name))
layout = self.form.bitParams.layout()
ui = FreeCADGui.UiLoader()
# for all properties either assign them to existing labels and editors
# or create additional ones for them if not enough have already been
# created.
usedRows = 0
for nr, name in enumerate(tool.Proxy.toolShapeProperties(tool)):
if nr < len(self.widgets):
Path.Log.debug("re-use row: {} [{}]".format(nr, name))
label, qsb, editor = self.widgets[nr]
label.setText(labelText(name))
editor.attachTo(tool, name)
label.show()
qsb.show()
else:
qsb = ui.createWidget("Gui::QuantitySpinBox")
editor = PathGui.QuantitySpinBox(qsb, tool, name)
label = QtGui.QLabel(labelText(name))
self.widgets.append((label, qsb, editor))
Path.Log.debug("create row: {} [{}] {}".format(nr, name, type(qsb)))
if hasattr(qsb, "editingFinished"):
qsb.editingFinished.connect(self.updateTool)
if nr >= layout.rowCount():
layout.addRow(label, qsb)
usedRows = usedRows + 1
# hide all rows which aren't being used
Path.Log.track(usedRows, len(self.widgets))
for i in range(usedRows, len(self.widgets)):
label, qsb, editor = self.widgets[i]
label.hide()
qsb.hide()
editor.attachTo(None)
Path.Log.debug(" hide row: {}".format(i))
img = tool.Proxy.getBitThumbnail(tool)
if img:
self.form.image.setPixmap(QtGui.QPixmap(QtGui.QImage.fromData(img)))
else:
self.form.image.setPixmap(QtGui.QPixmap())
def setupAttributes(self, tool):
Path.Log.track()
setup = True
if not hasattr(self, "delegate"):
self.delegate = _Delegate(self.form.attrTree)
self.model = QtGui.QStandardItemModel(self.form.attrTree)
self.model.setHorizontalHeaderLabels(["Property", "Value"])
else:
self.model.removeRows(0, self.model.rowCount())
setup = False
attributes = tool.Proxy.toolGroupsAndProperties(tool, False)
for name in attributes:
group = QtGui.QStandardItem()
group.setData(name, QtCore.Qt.EditRole)
group.setEditable(False)
for prop in attributes[name]:
label = QtGui.QStandardItem()
label.setData(prop, QtCore.Qt.EditRole)
label.setEditable(False)
value = QtGui.QStandardItem()
value.setData(
PathUtil.getPropertyValueString(tool, prop), QtCore.Qt.DisplayRole
)
value.setData(tool, _Delegate.ObjectRole)
value.setData(prop, _Delegate.PropertyRole)
group.appendRow([label, value])
self.model.appendRow(group)
if setup:
self.form.attrTree.setModel(self.model)
self.form.attrTree.setItemDelegateForColumn(1, self.delegate)
self.form.attrTree.expandAll()
self.form.attrTree.resizeColumnToContents(0)
self.form.attrTree.resizeColumnToContents(1)
# self.form.attrTree.collapseAll()
def accept(self):
Path.Log.track()
self.refresh()
self.tool.Proxy.unloadBitBody(self.tool)
def reject(self):
Path.Log.track()
self.tool.Proxy.unloadBitBody(self.tool)
def updateUI(self):
Path.Log.track()
self.form.toolName.setText(self.tool.Label)
self.form.shapePath.setText(self.tool.BitShape)
for lbl, qsb, editor in self.widgets:
editor.updateSpinBox()
def _updateBitShape(self, shapePath):
# Only need to go through this exercise if the shape actually changed.
if self.tool.BitShape != shapePath:
# Before setting a new bitshape we need to make sure that none of
# editors fires an event and tries to access its old property, which
# might not exist anymore.
for lbl, qsb, editor in self.widgets:
editor.attachTo(self.tool, "File")
self.tool.BitShape = shapePath
self.setupTool(self.tool)
self.form.toolName.setText(self.tool.Label)
if self.tool.BitBody and self.tool.BitBody.ViewObject:
if not self.tool.BitBody.ViewObject.Visibility:
self.tool.BitBody.ViewObject.Visibility = True
self.setupAttributes(self.tool)
return True
return False
def updateShape(self):
Path.Log.track()
shapePath = str(self.form.shapePath.text())
# Only need to go through this exercise if the shape actually changed.
if self._updateBitShape(shapePath):
for lbl, qsb, editor in self.widgets:
editor.updateSpinBox()
def updateTool(self):
Path.Log.track()
label = str(self.form.toolName.text())
shape = str(self.form.shapePath.text())
if self.tool.Label != label:
self.tool.Label = label
self._updateBitShape(shape)
for lbl, qsb, editor in self.widgets:
editor.updateProperty()
self.tool.Proxy._updateBitShape(self.tool)
def refresh(self):
Path.Log.track()
self.form.blockSignals(True)
self.updateTool()
self.updateUI()
self.form.blockSignals(False)
def selectShape(self):
Path.Log.track()
path = self.tool.BitShape
if not path:
path = PathPreferences.lastPathToolShape()
foo = QtGui.QFileDialog.getOpenFileName(
self.form, "Path - Tool Shape", path, "*.fcstd"
)
if foo and foo[0]:
PathPreferences.setLastPathToolShape(os.path.dirname(foo[0]))
self.form.shapePath.setText(foo[0])
self.updateShape()
def setupUI(self):
Path.Log.track()
self.updateUI()
self.form.toolName.editingFinished.connect(self.refresh)
self.form.shapePath.editingFinished.connect(self.updateShape)
self.form.shapeSet.clicked.connect(self.selectShape)

View File

@@ -1,272 +0,0 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2019 sliptonic <shopinthewoods@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
from PySide import QtCore, QtGui
from PySide.QtCore import QT_TRANSLATE_NOOP
import FreeCAD
import FreeCADGui
import Path
import PathScripts.PathIconViewProvider as PathIconViewProvider
import PathScripts.PathPreferences as PathPreferences
import PathScripts.PathToolBit as PathToolBit
import PathScripts.PathToolBitEdit as PathToolBitEdit
import os
__title__ = "Tool Bit UI"
__author__ = "sliptonic (Brad Collette)"
__url__ = "https://www.freecadweb.org"
__doc__ = "Task panel editor for a ToolBit"
if False:
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
else:
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
translate = FreeCAD.Qt.translate
class ViewProvider(object):
"""ViewProvider for a ToolBit.
It's sole job is to provide an icon and invoke the TaskPanel on edit."""
def __init__(self, vobj, name):
Path.Log.track(name, vobj.Object)
self.panel = None
self.icon = name
self.obj = vobj.Object
self.vobj = vobj
vobj.Proxy = self
def attach(self, vobj):
Path.Log.track(vobj.Object)
self.vobj = vobj
self.obj = vobj.Object
def getIcon(self):
png = self.obj.Proxy.getBitThumbnail(self.obj)
if png:
pixmap = QtGui.QPixmap()
pixmap.loadFromData(png, "PNG")
return QtGui.QIcon(pixmap)
return ":/icons/Path_ToolBit.svg"
def __getstate__(self):
return None
def __setstate__(self, state):
return None
def onDelete(self, vobj, arg2=None):
Path.Log.track(vobj.Object.Label)
vobj.Object.Proxy.onDelete(vobj.Object)
def getDisplayMode(self, mode):
return "Default"
def _openTaskPanel(self, vobj, deleteOnReject):
Path.Log.track()
self.panel = TaskPanel(vobj, deleteOnReject)
FreeCADGui.Control.closeDialog()
FreeCADGui.Control.showDialog(self.panel)
self.panel.setupUi()
def setCreate(self, vobj):
Path.Log.track()
self._openTaskPanel(vobj, True)
def setEdit(self, vobj, mode=0):
self._openTaskPanel(vobj, False)
return True
def unsetEdit(self, vobj, mode):
FreeCADGui.Control.closeDialog()
self.panel = None
return
def claimChildren(self):
if self.obj.BitBody:
return [self.obj.BitBody]
return []
def doubleClicked(self, vobj):
if os.path.exists(vobj.Object.BitShape):
self.setEdit(vobj)
else:
msg = translate(
"PathToolBit", "Toolbit cannot be edited: Shapefile not found"
)
diag = QtGui.QMessageBox(QtGui.QMessageBox.Warning, "Error", msg)
diag.setWindowModality(QtCore.Qt.ApplicationModal)
diag.exec_()
class TaskPanel:
"""TaskPanel for the SetupSheet - if it is being edited directly."""
def __init__(self, vobj, deleteOnReject):
Path.Log.track(vobj.Object.Label)
self.vobj = vobj
self.obj = vobj.Object
self.editor = PathToolBitEdit.ToolBitEditor(self.obj)
self.form = self.editor.form
self.deleteOnReject = deleteOnReject
FreeCAD.ActiveDocument.openTransaction("Edit ToolBit")
def reject(self):
FreeCAD.ActiveDocument.abortTransaction()
self.editor.reject()
FreeCADGui.Control.closeDialog()
if self.deleteOnReject:
FreeCAD.ActiveDocument.openTransaction("Uncreate ToolBit")
self.editor.reject()
FreeCAD.ActiveDocument.removeObject(self.obj.Name)
FreeCAD.ActiveDocument.commitTransaction()
FreeCAD.ActiveDocument.recompute()
def accept(self):
self.editor.accept()
FreeCAD.ActiveDocument.commitTransaction()
FreeCADGui.ActiveDocument.resetEdit()
FreeCADGui.Control.closeDialog()
FreeCAD.ActiveDocument.recompute()
def updateUI(self):
Path.Log.track()
self.editor.updateUI()
def updateModel(self):
self.editor.updateTool()
FreeCAD.ActiveDocument.recompute()
def setupUi(self):
self.editor.setupUI()
class ToolBitGuiFactory(PathToolBit.ToolBitFactory):
def Create(self, name="ToolBit", shapeFile=None, path=None):
"""Create(name = 'ToolBit') ... creates a new tool bit.
It is assumed the tool will be edited immediately so the internal bit body is still attached."""
Path.Log.track(name, shapeFile, path)
FreeCAD.ActiveDocument.openTransaction("Create ToolBit")
tool = PathToolBit.ToolBitFactory.Create(self, name, shapeFile, path)
PathIconViewProvider.Attach(tool.ViewObject, name)
FreeCAD.ActiveDocument.commitTransaction()
return tool
def isValidFileName(filename):
print(filename)
try:
with open(filename, "w") as tempfile:
return True
except Exception:
return False
def GetNewToolFile(parent=None):
if parent is None:
parent = QtGui.QApplication.activeWindow()
foo = QtGui.QFileDialog.getSaveFileName(
parent, "Tool", PathPreferences.lastPathToolBit(), "*.fctb"
)
if foo and foo[0]:
if not isValidFileName(foo[0]):
msgBox = QtGui.QMessageBox()
msg = translate("Path", "Invalid Filename")
msgBox.setText(msg)
msgBox.exec_()
else:
PathPreferences.setLastPathToolBit(os.path.dirname(foo[0]))
return foo[0]
return None
def GetToolFile(parent=None):
if parent is None:
parent = QtGui.QApplication.activeWindow()
foo = QtGui.QFileDialog.getOpenFileName(
parent, "Tool", PathPreferences.lastPathToolBit(), "*.fctb"
)
if foo and foo[0]:
PathPreferences.setLastPathToolBit(os.path.dirname(foo[0]))
return foo[0]
return None
def GetToolFiles(parent=None):
if parent is None:
parent = QtGui.QApplication.activeWindow()
foo = QtGui.QFileDialog.getOpenFileNames(
parent, "Tool", PathPreferences.lastPathToolBit(), "*.fctb"
)
if foo and foo[0]:
PathPreferences.setLastPathToolBit(os.path.dirname(foo[0][0]))
return foo[0]
return []
def GetToolShapeFile(parent=None):
if parent is None:
parent = QtGui.QApplication.activeWindow()
location = PathPreferences.lastPathToolShape()
if os.path.isfile(location):
location = os.path.split(location)[0]
elif not os.path.isdir(location):
location = PathPreferences.filePath()
fname = QtGui.QFileDialog.getOpenFileName(
parent, "Select Tool Shape", location, "*.fcstd"
)
if fname and fname[0]:
if fname != location:
newloc = os.path.dirname(fname[0])
PathPreferences.setLastPathToolShape(newloc)
return fname[0]
else:
return None
def LoadTool(parent=None):
"""
LoadTool(parent=None) ... Open a file dialog to load a tool from a file.
"""
foo = GetToolFile(parent)
return PathToolBit.Factory.CreateFrom(foo) if foo else foo
def LoadTools(parent=None):
"""
LoadTool(parent=None) ... Open a file dialog to load a tool from a file.
"""
return [PathToolBit.Factory.CreateFrom(foo) for foo in GetToolFiles(parent)]
# Set the factory so all tools are created with UI
PathToolBit.Factory = ToolBitGuiFactory()
PathIconViewProvider.RegisterViewProvider("ToolBit", ViewProvider)

View File

@@ -1,102 +0,0 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2019 sliptonic <shopinthewoods@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
from PySide.QtCore import QT_TRANSLATE_NOOP
import FreeCAD
import FreeCADGui
import Path
if False:
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
else:
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
translate = FreeCAD.Qt.translate
class CommandToolBitSelectorOpen:
"""
Command to toggle the ToolBitSelector Dock
"""
def __init__(self):
pass
def GetResources(self):
return {
"Pixmap": "Path_ToolTable",
"MenuText": QT_TRANSLATE_NOOP("Path_ToolBitDock", "ToolBit Dock"),
"ToolTip": QT_TRANSLATE_NOOP("Path_ToolBitDock", "Toggle the Toolbit Dock"),
"Accel": "P, T",
"CmdType": "ForEdit",
}
def IsActive(self):
return FreeCAD.ActiveDocument is not None
def Activated(self):
import PathScripts.PathToolBitLibraryGui as PathToolBitLibraryGui
dock = PathToolBitLibraryGui.ToolBitSelector()
dock.open()
class CommandToolBitLibraryOpen:
"""
Command to open ToolBitLibrary editor.
"""
def __init__(self):
pass
def GetResources(self):
return {
"Pixmap": "Path_ToolTable",
"MenuText": QT_TRANSLATE_NOOP(
"Path_ToolBitLibraryOpen", "ToolBit Library editor"
),
"ToolTip": QT_TRANSLATE_NOOP(
"Path_ToolBitLibraryOpen", "Open an editor to manage ToolBit libraries"
),
"CmdType": "ForEdit",
}
def IsActive(self):
return FreeCAD.ActiveDocument is not None
def Activated(self):
import PathScripts.PathToolBitLibraryGui as PathToolBitLibraryGui
library = PathToolBitLibraryGui.ToolBitLibrary()
library.open()
if FreeCAD.GuiUp:
FreeCADGui.addCommand("Path_ToolBitLibraryOpen", CommandToolBitLibraryOpen())
FreeCADGui.addCommand("Path_ToolBitDock", CommandToolBitSelectorOpen())
BarList = ["Path_ToolBitDock"]
MenuList = ["Path_ToolBitLibraryOpen", "Path_ToolBitDock"]
FreeCAD.Console.PrintLog("Loading PathToolBitLibraryCmd... done\n")

View File

@@ -1,966 +0,0 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2019 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2020 Schildkroet *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
import FreeCAD
import FreeCADGui
import Path
import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts.PathPreferences as PathPreferences
import PathScripts.PathToolBit as PathToolBit
import PathScripts.PathToolBitEdit as PathToolBitEdit
import PathScripts.PathToolBitGui as PathToolBitGui
import PathScripts.PathToolControllerGui as PathToolControllerGui
import PathScripts.PathUtilsGui as PathUtilsGui
import PySide
import glob
import json
import os
import shutil
import uuid as UUID
from functools import partial
if False:
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
else:
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
_UuidRole = PySide.QtCore.Qt.UserRole + 1
_PathRole = PySide.QtCore.Qt.UserRole + 2
translate = FreeCAD.Qt.translate
def checkWorkingDir():
# users shouldn't use the example toolbits and libraries.
# working directory should be writable
Path.Log.track()
workingdir = os.path.dirname(PathPreferences.lastPathToolLibrary())
defaultdir = os.path.dirname(PathPreferences.pathDefaultToolsPath())
Path.Log.debug("workingdir: {} defaultdir: {}".format(workingdir, defaultdir))
dirOK = lambda: workingdir != defaultdir and (os.access(workingdir, os.W_OK))
if dirOK():
return True
qm = PySide.QtGui.QMessageBox
ret = qm.question(
None,
"",
translate("Path_ToolBit", "Toolbit working directory not set up. Do that now?"),
qm.Yes | qm.No,
)
if ret == qm.No:
return False
msg = translate("Path_ToolBit", "Choose a writable location for your toolbits")
while not dirOK():
workingdir = PySide.QtGui.QFileDialog.getExistingDirectory(
None, msg, PathPreferences.filePath()
)
if workingdir[-8:] == os.path.sep + "Library":
workingdir = workingdir[:-8] # trim off trailing /Library if user chose it
PathPreferences.setLastPathToolLibrary(
"{}{}Library".format(workingdir, os.path.sep)
)
PathPreferences.setLastPathToolBit("{}{}Bit".format(workingdir, os.path.sep))
Path.Log.debug("setting workingdir to: {}".format(workingdir))
# Copy only files of default Path\Tools folder to working directory (targeting the README.md help file)
src_toolfiles = os.listdir(defaultdir)
for file_name in src_toolfiles:
if file_name in ["README.md"]:
full_file_name = os.path.join(defaultdir, file_name)
if os.path.isfile(full_file_name):
shutil.copy(full_file_name, workingdir)
# Determine which subdirectories are missing
subdirlist = ["Bit", "Library", "Shape"]
mode = 0o777
for dir in subdirlist.copy():
subdir = "{}{}{}".format(workingdir, os.path.sep, dir)
if os.path.exists(subdir):
subdirlist.remove(dir)
# Query user for creation permission of any missing subdirectories
if len(subdirlist) >= 1:
needed = ", ".join([str(d) for d in subdirlist])
qm = PySide.QtGui.QMessageBox
ret = qm.question(
None,
"",
translate(
"Path_ToolBit",
"Toolbit Working directory {} needs these sudirectories:\n {} \n Create them?",
).format(workingdir, needed),
qm.Yes | qm.No,
)
if ret == qm.No:
return False
else:
# Create missing subdirectories if user agrees to creation
for dir in subdirlist:
subdir = "{}{}{}".format(workingdir, os.path.sep, dir)
os.mkdir(subdir, mode)
# Query user to copy example files into subdirectories created
if dir != "Shape":
qm = PySide.QtGui.QMessageBox
ret = qm.question(
None,
"",
translate(
"Path_ToolBit", "Copy example files to new {} directory?"
).format(dir),
qm.Yes | qm.No,
)
if ret == qm.Yes:
src = "{}{}{}".format(defaultdir, os.path.sep, dir)
src_files = os.listdir(src)
for file_name in src_files:
full_file_name = os.path.join(src, file_name)
if os.path.isfile(full_file_name):
shutil.copy(full_file_name, subdir)
# if no library is set, choose the first one in the Library directory
if PathPreferences.lastFileToolLibrary() is None:
libFiles = [
f
for f in glob.glob(
PathPreferences.lastPathToolLibrary() + os.path.sep + "*.fctl"
)
]
PathPreferences.setLastFileToolLibrary(libFiles[0])
return True
class _TableView(PySide.QtGui.QTableView):
"""Subclass of QTableView to support rearrange and copying of ToolBits"""
def __init__(self, parent):
PySide.QtGui.QTableView.__init__(self, parent)
self.setDragEnabled(False)
self.setAcceptDrops(False)
self.setDropIndicatorShown(False)
self.setDragDropMode(PySide.QtGui.QAbstractItemView.DragOnly)
self.setDefaultDropAction(PySide.QtCore.Qt.IgnoreAction)
self.setSortingEnabled(True)
self.setSelectionBehavior(PySide.QtGui.QAbstractItemView.SelectRows)
self.verticalHeader().hide()
def supportedDropActions(self):
return [PySide.QtCore.Qt.CopyAction, PySide.QtCore.Qt.MoveAction]
def _uuidOfRow(self, row):
model = self.toolModel()
return model.data(model.index(row, 0), _UuidRole)
def _rowWithUuid(self, uuid):
model = self.toolModel()
for row in range(model.rowCount()):
if self._uuidOfRow(row) == uuid:
return row
return None
def _copyTool(self, uuid_, dstRow):
model = self.toolModel()
model.insertRow(dstRow)
srcRow = self._rowWithUuid(uuid_)
for col in range(model.columnCount()):
srcItem = model.item(srcRow, col)
model.setData(
model.index(dstRow, col),
srcItem.data(PySide.QtCore.Qt.EditRole),
PySide.QtCore.Qt.EditRole,
)
if col == 0:
model.setData(
model.index(dstRow, col), srcItem.data(_PathRole), _PathRole
)
# Even a clone of a tool gets its own uuid so it can be identified when
# rearranging the order or inserting/deleting rows
model.setData(model.index(dstRow, col), UUID.uuid4(), _UuidRole)
else:
model.item(dstRow, col).setEditable(False)
def _copyTools(self, uuids, dst):
for i, uuid in enumerate(uuids):
self._copyTool(uuid, dst + i)
def dropEvent(self, event):
Path.Log.track()
mime = event.mimeData()
data = mime.data("application/x-qstandarditemmodeldatalist")
stream = PySide.QtCore.QDataStream(data)
srcRows = []
while not stream.atEnd():
row = stream.readInt32()
srcRows.append(row)
# get the uuids of all srcRows
model = self.toolModel()
srcUuids = [self._uuidOfRow(row) for row in set(srcRows)]
destRow = self.rowAt(event.pos().y())
self._copyTools(srcUuids, destRow)
if PySide.QtCore.Qt.DropAction.MoveAction == event.proposedAction():
for uuid in srcUuids:
model.removeRow(self._rowWithUuid(uuid))
class ModelFactory(object):
"""Helper class to generate qtdata models for toolbit libraries"""
def __init__(self, path=None):
Path.Log.track()
self.path = ""
# self.currentLib = ""
def __libraryLoad(self, path, datamodel):
Path.Log.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:
Path.Log.track(bit)
tool = PathToolBit.Declaration(bit)
datamodel.appendRow(self._toolAdd(nr, tool, bit))
else:
Path.Log.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)
toolNr = PySide.QtGui.QStandardItem()
toolNr.setData(nr, PySide.QtCore.Qt.EditRole)
toolNr.setToolTip(tool["shape"])
toolNr.setData(path, _PathRole)
toolNr.setData(UUID.uuid4(), _UuidRole)
toolNr.setToolTip(tooltip)
toolName = PySide.QtGui.QStandardItem()
toolName.setData(tool["name"], PySide.QtCore.Qt.EditRole)
toolName.setEditable(False)
toolName.setToolTip(tooltip)
toolShape = PySide.QtGui.QStandardItem()
toolShape.setData(strShape, PySide.QtCore.Qt.EditRole)
toolShape.setEditable(False)
return [toolNr, toolName, toolShape]
def newTool(self, datamodel, path):
"""
Adds a toolbit item to a model
"""
Path.Log.track()
try:
nr = 0
for row in range(datamodel.rowCount()):
itemNr = int(datamodel.item(row, 0).data(PySide.QtCore.Qt.EditRole))
nr = max(nr, itemNr)
nr += 1
tool = PathToolBit.Declaration(path)
except Exception as e:
Path.Log.error(e)
datamodel.appendRow(self._toolAdd(nr, tool, path))
def findLibraries(self, model):
"""
Finds all the fctl files in a location
Returns a QStandardItemModel
"""
Path.Log.track()
path = PathPreferences.lastPathToolLibrary()
if os.path.isdir(path): # opening all tables in a directory
libFiles = [f for f in glob.glob(path + os.path.sep + "*.fctl")]
libFiles.sort()
for libFile in libFiles:
loc, fnlong = os.path.split(libFile)
fn, ext = os.path.splitext(fnlong)
libItem = PySide.QtGui.QStandardItem(fn)
libItem.setToolTip(loc)
libItem.setData(libFile, _PathRole)
libItem.setIcon(PySide.QtGui.QPixmap(":/icons/Path_ToolTable.svg"))
model.appendRow(libItem)
Path.Log.debug("model rows: {}".format(model.rowCount()))
return model
def libraryOpen(self, model, lib=""):
"""
opens the tools in library
Returns a QStandardItemModel
"""
Path.Log.track(lib)
if lib == "":
lib = PathPreferences.lastFileToolLibrary()
if lib == "" or lib is None:
return model
if os.path.isfile(lib): # An individual library is wanted
self.__libraryLoad(lib, model)
Path.Log.debug("model rows: {}".format(model.rowCount()))
return model
class ToolBitSelector(object):
"""Controller for displaying a library and creating ToolControllers"""
def __init__(self):
checkWorkingDir()
self.form = FreeCADGui.PySideUic.loadUi(":/panels/ToolBitSelector.ui")
self.factory = ModelFactory()
self.toolModel = PySide.QtGui.QStandardItemModel(0, len(self.columnNames()))
self.setupUI()
self.title = self.form.windowTitle()
def columnNames(self):
return ["#", "Tool"]
def currentLibrary(self, shortNameOnly):
libfile = PathPreferences.lastFileToolLibrary()
if libfile is None or libfile == "":
return ""
elif shortNameOnly:
return os.path.splitext(os.path.basename(libfile))[0]
return libfile
def loadData(self):
Path.Log.track()
self.toolModel.clear()
self.toolModel.setHorizontalHeaderLabels(self.columnNames())
self.form.lblLibrary.setText(self.currentLibrary(True))
self.form.lblLibrary.setToolTip(self.currentLibrary(False))
self.factory.libraryOpen(self.toolModel)
self.toolModel.takeColumn(3)
self.toolModel.takeColumn(2)
def setupUI(self):
Path.Log.track()
self.loadData()
self.form.tools.setModel(self.toolModel)
self.form.tools.selectionModel().selectionChanged.connect(self.enableButtons)
self.form.tools.doubleClicked.connect(
partial(self.selectedOrAllToolControllers)
)
self.form.libraryEditorOpen.clicked.connect(self.libraryEditorOpen)
self.form.addToolController.clicked.connect(self.selectedOrAllToolControllers)
def enableButtons(self):
selected = len(self.form.tools.selectedIndexes()) >= 1
if selected:
jobs = (
len([1 for j in FreeCAD.ActiveDocument.Objects if j.Name[:3] == "Job"])
>= 1
)
self.form.addToolController.setEnabled(selected and jobs)
def libraryEditorOpen(self):
library = ToolBitLibrary()
library.open()
self.loadData()
def selectedOrAllTools(self):
"""
Iterate the selection and add individual tools
If a group is selected, iterate and add children
"""
itemsToProcess = []
for index in self.form.tools.selectedIndexes():
item = index.model().itemFromIndex(index)
if item.hasChildren():
for i in range(item.rowCount() - 1):
if item.child(i).column() == 0:
itemsToProcess.append(item.child(i))
elif item.column() == 0:
itemsToProcess.append(item)
tools = []
for item in itemsToProcess:
toolNr = int(item.data(PySide.QtCore.Qt.EditRole))
toolPath = item.data(_PathRole)
tools.append((toolNr, PathToolBit.Factory.CreateFrom(toolPath)))
return tools
def selectedOrAllToolControllers(self, index=None):
"""
if no jobs, don't do anything, otherwise all TCs for all
selected toolbits
"""
jobs = PathUtilsGui.PathUtils.GetJobs()
if len(jobs) == 0:
return
elif len(jobs) == 1:
job = jobs[0]
else:
userinput = PathUtilsGui.PathUtilsUserInput()
job = userinput.chooseJob(jobs)
if job is None: # user may have canceled
return
tools = self.selectedOrAllTools()
for tool in tools:
tc = PathToolControllerGui.Create(
"TC: {}".format(tool[1].Label), tool[1], tool[0]
)
job.Proxy.addToolController(tc)
FreeCAD.ActiveDocument.recompute()
def open(self, path=None):
"""load library stored in path and bring up ui"""
docs = FreeCADGui.getMainWindow().findChildren(PySide.QtGui.QDockWidget)
for doc in docs:
if doc.objectName() == "ToolSelector":
if doc.isVisible():
doc.deleteLater()
return
else:
doc.setVisible(True)
return
mw = FreeCADGui.getMainWindow()
mw.addDockWidget(
PySide.QtCore.Qt.RightDockWidgetArea,
self.form,
PySide.QtCore.Qt.Orientation.Vertical,
)
class ToolBitLibrary(object):
"""ToolBitLibrary is the controller for
displaying/selecting/creating/editing a collection of ToolBits."""
def __init__(self):
Path.Log.track()
checkWorkingDir()
self.factory = ModelFactory()
self.temptool = None
self.toolModel = PySide.QtGui.QStandardItemModel(0, len(self.columnNames()))
self.listModel = PySide.QtGui.QStandardItemModel()
self.form = FreeCADGui.PySideUic.loadUi(":/panels/ToolBitLibraryEdit.ui")
self.toolTableView = _TableView(self.form.toolTableGroup)
self.form.toolTableGroup.layout().replaceWidget(
self.form.toolTable, self.toolTableView
)
self.form.toolTable.hide()
self.setupUI()
self.title = self.form.windowTitle()
def toolBitNew(self):
Path.Log.track()
# select the shape file
shapefile = PathToolBitGui.GetToolShapeFile()
if shapefile is None: # user canceled
return
# select the bit file location and filename
filename = PathToolBitGui.GetNewToolFile()
if filename is None:
return
# Parse out the name of the file and write the structure
loc, fil = os.path.split(filename)
fname = os.path.splitext(fil)[0]
fullpath = "{}{}{}.fctb".format(loc, os.path.sep, fname)
Path.Log.debug("fullpath: {}".format(fullpath))
self.temptool = PathToolBit.ToolBitFactory().Create(name=fname)
self.temptool.BitShape = shapefile
self.temptool.Proxy.unloadBitBody(self.temptool)
self.temptool.Label = fname
self.temptool.Proxy.saveToFile(self.temptool, fullpath)
self.temptool.Document.removeObject(self.temptool.Name)
self.temptool = None
# add it to the model
self.factory.newTool(self.toolModel, fullpath)
def toolBitExisting(self):
filenames = PathToolBitGui.GetToolFiles()
if len(filenames) == 0:
return
for f in filenames:
loc, fil = os.path.split(f)
fname = os.path.splitext(fil)[0]
fullpath = "{}{}{}.fctb".format(loc, os.path.sep, fname)
self.factory.newTool(self.toolModel, fullpath)
def toolDelete(self):
Path.Log.track()
selectedRows = set(
[index.row() for index in self.toolTableView.selectedIndexes()]
)
for row in sorted(list(selectedRows), key=lambda r: -r):
self.toolModel.removeRows(row, 1)
def toolSelect(self, selected, deselected):
sel = len(self.toolTableView.selectedIndexes()) > 0
self.form.toolDelete.setEnabled(sel)
def tableSelected(self, index):
"""loads the tools for the selected tool table"""
Path.Log.track()
item = index.model().itemFromIndex(index)
libpath = item.data(_PathRole)
self.loadData(libpath)
self.path = libpath
def open(self):
Path.Log.track()
return self.form.exec_()
def libraryPath(self):
Path.Log.track()
path = PySide.QtGui.QFileDialog.getExistingDirectory(
self.form, "Tool Library Path", PathPreferences.lastPathToolLibrary()
)
if len(path) == 0:
return
PathPreferences.setLastPathToolLibrary(path)
self.loadData()
def cleanupDocument(self):
# This feels like a hack. Remove the toolbit object
# remove the editor from the dialog
# re-enable all the controls
self.temptool.Proxy.unloadBitBody(self.temptool)
self.temptool.Document.removeObject(self.temptool.Name)
self.temptool = None
widget = self.form.toolTableGroup.children()[-1]
widget.setParent(None)
self.editor = None
self.lockoff()
def accept(self):
self.editor.accept()
self.temptool.Proxy.saveToFile(self.temptool, self.temptool.File)
self.librarySave()
self.loadData()
self.cleanupDocument()
def reject(self):
self.cleanupDocument()
def lockon(self):
self.toolTableView.setEnabled(False)
self.form.toolCreate.setEnabled(False)
self.form.toolDelete.setEnabled(False)
self.form.toolAdd.setEnabled(False)
self.form.TableList.setEnabled(False)
self.form.libraryOpen.setEnabled(False)
self.form.libraryExport.setEnabled(False)
self.form.addToolTable.setEnabled(False)
self.form.librarySave.setEnabled(False)
def lockoff(self):
self.toolTableView.setEnabled(True)
self.form.toolCreate.setEnabled(True)
self.form.toolDelete.setEnabled(True)
self.form.toolAdd.setEnabled(True)
self.form.toolTable.setEnabled(True)
self.form.TableList.setEnabled(True)
self.form.libraryOpen.setEnabled(True)
self.form.libraryExport.setEnabled(True)
self.form.addToolTable.setEnabled(True)
self.form.librarySave.setEnabled(True)
def toolEdit(self, selected):
Path.Log.track()
item = self.toolModel.item(selected.row(), 0)
if self.temptool is not None:
self.temptool.Document.removeObject(self.temptool.Name)
if selected.column() == 0: # editing Nr
pass
else:
tbpath = item.data(_PathRole)
self.temptool = PathToolBit.ToolBitFactory().CreateFrom(tbpath, "temptool")
self.editor = PathToolBitEdit.ToolBitEditor(
self.temptool, self.form.toolTableGroup, loadBitBody=False
)
QBtn = (
PySide.QtGui.QDialogButtonBox.Ok | PySide.QtGui.QDialogButtonBox.Cancel
)
buttonBox = PySide.QtGui.QDialogButtonBox(QBtn)
buttonBox.accepted.connect(self.accept)
buttonBox.rejected.connect(self.reject)
layout = self.editor.form.layout()
layout.addWidget(buttonBox)
self.lockon()
self.editor.setupUI()
def toolEditDone(self, success=True):
FreeCAD.ActiveDocument.removeObject("temptool")
print("all done")
def libraryNew(self):
TooltableTypeJSON = translate("Path_ToolBit", "Tooltable JSON (*.fctl)")
filename = PySide.QtGui.QFileDialog.getSaveFileName(
self.form,
translate("Path_ToolBit", "Save toolbit library"),
PathPreferences.lastPathToolLibrary(),
"{}".format(TooltableTypeJSON),
)
if not (filename and filename[0]):
self.loadData()
path = (
filename[0]
if filename[0].endswith(".fctl")
else "{}.fctl".format(filename[0])
)
library = {}
tools = []
library["version"] = 1
library["tools"] = tools
with open(path, "w") as fp:
json.dump(library, fp, sort_keys=True, indent=2)
self.loadData()
def librarySave(self):
library = {}
tools = []
library["version"] = 1
library["tools"] = tools
for row in range(self.toolModel.rowCount()):
toolNr = self.toolModel.data(
self.toolModel.index(row, 0), PySide.QtCore.Qt.EditRole
)
toolPath = self.toolModel.data(self.toolModel.index(row, 0), _PathRole)
if PathPreferences.toolsStoreAbsolutePaths():
bitPath = toolPath
else:
# bitPath = PathToolBit.findRelativePathTool(toolPath)
# Extract the name of the shape file
__, filShp = os.path.split(
toolPath
) # __ is an ignored placeholder acknowledged by LGTM
bitPath = str(filShp)
tools.append({"nr": toolNr, "path": bitPath})
if self.path is not None:
with open(self.path, "w") as fp:
json.dump(library, fp, sort_keys=True, indent=2)
def libraryOk(self):
self.librarySave()
self.form.close()
def libPaths(self):
lib = PathPreferences.lastFileToolLibrary()
loc = PathPreferences.lastPathToolLibrary()
Path.Log.track("lib: {} loc: {}".format(lib, loc))
return lib, loc
def columnNames(self):
return ["Nr", "Tool", "Shape"]
def loadData(self, path=None):
Path.Log.track(path)
self.toolTableView.setUpdatesEnabled(False)
self.form.TableList.setUpdatesEnabled(False)
if path is None:
path, loc = self.libPaths()
self.toolModel.clear()
self.listModel.clear()
self.factory.libraryOpen(self.toolModel, lib=path)
self.factory.findLibraries(self.listModel)
else:
self.toolModel.clear()
self.factory.libraryOpen(self.toolModel, lib=path)
self.path = path
self.form.setWindowTitle("{}".format(PathPreferences.lastPathToolLibrary()))
self.toolModel.setHorizontalHeaderLabels(self.columnNames())
self.listModel.setHorizontalHeaderLabels(["Library"])
# Select the current library in the list of tables
curIndex = None
for i in range(self.listModel.rowCount()):
item = self.listModel.item(i)
if item.data(_PathRole) == path:
curIndex = self.listModel.indexFromItem(item)
if curIndex:
sm = self.form.TableList.selectionModel()
sm.select(curIndex, PySide.QtCore.QItemSelectionModel.Select)
self.toolTableView.setUpdatesEnabled(True)
self.form.TableList.setUpdatesEnabled(True)
def setupUI(self):
Path.Log.track()
self.form.TableList.setModel(self.listModel)
self.toolTableView.setModel(self.toolModel)
self.loadData()
self.toolTableView.resizeColumnsToContents()
self.toolTableView.selectionModel().selectionChanged.connect(self.toolSelect)
self.toolTableView.doubleClicked.connect(self.toolEdit)
self.form.TableList.clicked.connect(self.tableSelected)
self.form.toolAdd.clicked.connect(self.toolBitExisting)
self.form.toolDelete.clicked.connect(self.toolDelete)
self.form.toolCreate.clicked.connect(self.toolBitNew)
self.form.addToolTable.clicked.connect(self.libraryNew)
self.form.libraryOpen.clicked.connect(self.libraryPath)
self.form.librarySave.clicked.connect(self.libraryOk)
self.form.libraryExport.clicked.connect(self.librarySaveAs)
self.toolSelect([], [])
def librarySaveAs(self, path):
TooltableTypeJSON = translate("Path_ToolBit", "Tooltable JSON (*.fctl)")
TooltableTypeLinuxCNC = translate("Path_ToolBit", "LinuxCNC tooltable (*.tbl)")
TooltableTypeCamotics = translate("Path_ToolBit", "Camotics tooltable (*.json)")
filename = PySide.QtGui.QFileDialog.getSaveFileName(
self.form,
translate("Path_ToolBit", "Save toolbit library"),
PathPreferences.lastPathToolLibrary(),
"{};;{};;{}".format(
TooltableTypeJSON, TooltableTypeLinuxCNC, TooltableTypeCamotics
),
)
if filename and filename[0]:
if filename[1] == TooltableTypeLinuxCNC:
path = (
filename[0]
if filename[0].endswith(".tbl")
else "{}.tbl".format(filename[0])
)
self.libararySaveLinuxCNC(path)
elif filename[1] == TooltableTypeCamotics:
path = (
filename[0]
if filename[0].endswith(".json")
else "{}.json".format(filename[0])
)
self.libararySaveCamotics(path)
else:
path = (
filename[0]
if filename[0].endswith(".fctl")
else "{}.fctl".format(filename[0])
)
self.path = path
self.librarySave()
self.updateToolbar()
def libararySaveLinuxCNC(self, path):
# linuxcnc line template
LIN = "T{} P{} X{} Y{} Z{} A{} B{} C{} U{} V{} W{} D{} I{} J{} Q{}; {}"
with open(path, "w") as fp:
fp.write(";\n")
for row in range(self.toolModel.rowCount()):
toolNr = self.toolModel.data(
self.toolModel.index(row, 0), PySide.QtCore.Qt.EditRole
)
toolPath = self.toolModel.data(self.toolModel.index(row, 0), _PathRole)
bit = PathToolBit.Factory.CreateFrom(toolPath)
if bit:
Path.Log.track(bit)
pocket = bit.Pocket if hasattr(bit, "Pocket") else "0"
xoffset = bit.Xoffset if hasattr(bit, "Xoffset") else "0"
yoffset = bit.Yoffset if hasattr(bit, "Yoffset") else "0"
zoffset = bit.Zoffset if hasattr(bit, "Zoffset") else "0"
aoffset = bit.Aoffset if hasattr(bit, "Aoffset") else "0"
boffset = bit.Boffset if hasattr(bit, "Boffset") else "0"
coffset = bit.Coffset if hasattr(bit, "Coffset") else "0"
uoffset = bit.Uoffset if hasattr(bit, "Uoffset") else "0"
voffset = bit.Voffset if hasattr(bit, "Voffset") else "0"
woffset = bit.Woffset if hasattr(bit, "Woffset") else "0"
diameter = (
bit.Diameter.getUserPreferred()[0].split()[0]
if hasattr(bit, "Diameter")
else "0"
)
frontangle = bit.FrontAngle if hasattr(bit, "FrontAngle") else "0"
backangle = bit.BackAngle if hasattr(bit, "BackAngle") else "0"
orientation = (
bit.Orientation if hasattr(bit, "Orientation") else "0"
)
remark = bit.Label
fp.write(
LIN.format(
toolNr,
pocket,
xoffset,
yoffset,
zoffset,
aoffset,
boffset,
coffset,
uoffset,
voffset,
woffset,
diameter,
frontangle,
backangle,
orientation,
remark,
)
+ "\n"
)
FreeCAD.ActiveDocument.removeObject(bit.Name)
else:
Path.Log.error("Could not find tool #{} ".format(toolNr))
def libararySaveCamotics(self, path):
SHAPEMAP = {
"ballend": "Ballnose",
"endmill": "Cylindrical",
"v-bit": "Conical",
"chamfer": "Snubnose",
}
tooltemplate = {
"units": "metric",
"shape": "cylindrical",
"length": 10,
"diameter": 3.125,
"description": "",
}
toollist = {}
unitstring = (
"imperial" if FreeCAD.Units.getSchema() in [2, 3, 5, 7] else "metric"
)
for row in range(self.toolModel.rowCount()):
toolNr = self.toolModel.data(
self.toolModel.index(row, 0), PySide.QtCore.Qt.EditRole
)
toolPath = self.toolModel.data(self.toolModel.index(row, 0), _PathRole)
Path.Log.debug(toolPath)
try:
bit = PathToolBit.Factory.CreateFrom(toolPath)
except FileNotFoundError as e:
FreeCAD.Console.PrintError(e)
continue
except Exception as e:
raise e
if not bit:
continue
Path.Log.track(bit)
toolitem = tooltemplate.copy()
toolitem["diameter"] = (
float(bit.Diameter.getUserPreferred()[0].split()[0])
if hasattr(bit, "Diameter")
else 2
)
toolitem["description"] = bit.Label
toolitem["length"] = (
float(bit.Length.getUserPreferred()[0].split()[0])
if hasattr(bit, "Length")
else 10
)
if hasattr(bit, "Camotics"):
toolitem["shape"] = bit.Camotics
else:
toolitem["shape"] = SHAPEMAP.get(bit.ShapeName, "Cylindrical")
toolitem["units"] = unitstring
FreeCAD.ActiveDocument.removeObject(bit.Name)
toollist[toolNr] = toolitem
if len(toollist) > 0:
with open(path, "w") as fp:
fp.write(json.dumps(toollist, indent=2))

View File

@@ -1,391 +0,0 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2015 Dan Falck <ddfalck@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
"""Tool Controller defines tool, spindle speed and feed rates for Path Operations"""
from PySide.QtCore import QT_TRANSLATE_NOOP
import FreeCAD
import Path
import PathScripts.PathPreferences as PathPreferences
import PathScripts.PathToolBit as PathToolBit
from Generators import toolchange_generator as toolchange_generator
from Generators.toolchange_generator import SpindleDirection
if False:
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
else:
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
translate = FreeCAD.Qt.translate
class ToolControllerTemplate:
"""Attribute and sub element strings for template export/import."""
Expressions = "xengine"
ExprExpr = "expr"
ExprProp = "prop"
HorizFeed = "hfeed"
HorizRapid = "hrapid"
Label = "label"
Name = "name"
SpindleDir = "dir"
SpindleSpeed = "speed"
ToolNumber = "nr"
Tool = "tool"
Version = "version"
VertFeed = "vfeed"
VertRapid = "vrapid"
class ToolController:
def __init__(self, obj, legacyTool=False, createTool=True):
Path.Log.track("tool: {}".format(legacyTool))
obj.addProperty(
"App::PropertyIntegerConstraint",
"ToolNumber",
"Tool",
QT_TRANSLATE_NOOP("App::Property", "The active tool"),
)
obj.ToolNumber = (0, 0, 10000, 1)
obj.addProperty(
"App::PropertyFloat",
"SpindleSpeed",
"Tool",
QT_TRANSLATE_NOOP(
"App::Property", "The speed of the cutting spindle in RPM"
),
)
obj.addProperty(
"App::PropertyEnumeration",
"SpindleDir",
"Tool",
QT_TRANSLATE_NOOP("App::Property", "Direction of spindle rotation"),
)
obj.addProperty(
"App::PropertySpeed",
"VertFeed",
"Feed",
QT_TRANSLATE_NOOP("App::Property", "Feed rate for vertical moves in Z"),
)
obj.addProperty(
"App::PropertySpeed",
"HorizFeed",
"Feed",
QT_TRANSLATE_NOOP("App::Property", "Feed rate for horizontal moves"),
)
obj.addProperty(
"App::PropertySpeed",
"VertRapid",
"Rapid",
QT_TRANSLATE_NOOP("App::Property", "Rapid rate for vertical moves in Z"),
)
obj.addProperty(
"App::PropertySpeed",
"HorizRapid",
"Rapid",
QT_TRANSLATE_NOOP("App::Property", "Rapid rate for horizontal moves"),
)
obj.setEditorMode("Placement", 2)
for n in self.propertyEnumerations():
setattr(obj, n[0], n[1])
if createTool:
self.ensureUseLegacyTool(obj, legacyTool)
@classmethod
def propertyEnumerations(self, dataType="data"):
"""helixOpPropertyEnumerations(dataType="data")... return property enumeration lists of specified dataType.
Args:
dataType = 'data', 'raw', 'translated'
Notes:
'data' is list of internal string literals used in code
'raw' is list of (translated_text, data_string) tuples
'translated' is list of translated string literals
"""
# Enumeration lists for App::PropertyEnumeration properties
enums = {
"SpindleDir": [
(translate("Path_ToolController", "Forward"), "Forward"),
(translate("Path_ToolController", "Reverse"), "Reverse"),
(translate("Path_ToolController", "None"), "None"),
], # this is the direction that the profile runs
}
if dataType == "raw":
return enums
data = list()
idx = 0 if dataType == "translated" else 1
Path.Log.debug(enums)
for k, v in enumerate(enums):
data.append((v, [tup[idx] for tup in enums[v]]))
Path.Log.debug(data)
return data
def onDocumentRestored(self, obj):
obj.setEditorMode("Placement", 2)
def onDelete(self, obj, arg2=None):
if not self.usesLegacyTool(obj):
if hasattr(obj.Tool, "InList") and len(obj.Tool.InList) == 1:
if hasattr(obj.Tool.Proxy, "onDelete"):
obj.Tool.Proxy.onDelete(obj.Tool)
def setFromTemplate(self, obj, template):
"""
setFromTemplate(obj, xmlItem) ... extract properties from xmlItem
and assign to receiver.
"""
Path.Log.track(obj.Name, template)
version = 0
if template.get(ToolControllerTemplate.Version):
version = int(template.get(ToolControllerTemplate.Version))
if version == 1 or version == 2:
if template.get(ToolControllerTemplate.Label):
obj.Label = template.get(ToolControllerTemplate.Label)
if template.get(ToolControllerTemplate.VertFeed):
obj.VertFeed = template.get(ToolControllerTemplate.VertFeed)
if template.get(ToolControllerTemplate.HorizFeed):
obj.HorizFeed = template.get(ToolControllerTemplate.HorizFeed)
if template.get(ToolControllerTemplate.VertRapid):
obj.VertRapid = template.get(ToolControllerTemplate.VertRapid)
if template.get(ToolControllerTemplate.HorizRapid):
obj.HorizRapid = template.get(ToolControllerTemplate.HorizRapid)
if template.get(ToolControllerTemplate.SpindleSpeed):
obj.SpindleSpeed = float(
template.get(ToolControllerTemplate.SpindleSpeed)
)
if template.get(ToolControllerTemplate.SpindleDir):
obj.SpindleDir = template.get(ToolControllerTemplate.SpindleDir)
if template.get(ToolControllerTemplate.ToolNumber):
obj.ToolNumber = int(
template.get(ToolControllerTemplate.ToolNumber)
)
if template.get(ToolControllerTemplate.Tool):
toolVersion = template.get(ToolControllerTemplate.Tool).get(
ToolControllerTemplate.Version
)
if toolVersion == 1:
self.ensureUseLegacyTool(obj, True)
obj.Tool.setFromTemplate(
template.get(ToolControllerTemplate.Tool)
)
else:
self.ensureUseLegacyTool(obj, False)
obj.Tool = PathToolBit.Factory.CreateFromAttrs(
template.get(ToolControllerTemplate.Tool)
)
if (
obj.Tool
and obj.Tool.ViewObject
and obj.Tool.ViewObject.Visibility
):
obj.Tool.ViewObject.Visibility = False
if template.get(ToolControllerTemplate.Expressions):
for exprDef in template.get(ToolControllerTemplate.Expressions):
if exprDef[ToolControllerTemplate.ExprExpr]:
obj.setExpression(
exprDef[ToolControllerTemplate.ExprProp],
exprDef[ToolControllerTemplate.ExprExpr],
)
else:
Path.Log.error(
"Unsupported PathToolController template version {}".format(
template.get(ToolControllerTemplate.Version)
)
)
else:
Path.Log.error(
"PathToolController template has no version - corrupted template file?"
)
def templateAttrs(self, obj):
"""templateAttrs(obj) ... answer a dictionary with all properties that should be stored for a template."""
attrs = {}
attrs[ToolControllerTemplate.Version] = 1
attrs[ToolControllerTemplate.Name] = obj.Name
attrs[ToolControllerTemplate.Label] = obj.Label
attrs[ToolControllerTemplate.ToolNumber] = obj.ToolNumber
attrs[ToolControllerTemplate.VertFeed] = "%s" % (obj.VertFeed)
attrs[ToolControllerTemplate.HorizFeed] = "%s" % (obj.HorizFeed)
attrs[ToolControllerTemplate.VertRapid] = "%s" % (obj.VertRapid)
attrs[ToolControllerTemplate.HorizRapid] = "%s" % (obj.HorizRapid)
attrs[ToolControllerTemplate.SpindleSpeed] = obj.SpindleSpeed
attrs[ToolControllerTemplate.SpindleDir] = obj.SpindleDir
if self.usesLegacyTool(obj):
attrs[ToolControllerTemplate.Tool] = obj.Tool.templateAttrs()
else:
attrs[ToolControllerTemplate.Tool] = obj.Tool.Proxy.templateAttrs(obj.Tool)
expressions = []
for expr in obj.ExpressionEngine:
Path.Log.debug("%s: %s" % (expr[0], expr[1]))
expressions.append(
{
ToolControllerTemplate.ExprProp: expr[0],
ToolControllerTemplate.ExprExpr: expr[1],
}
)
if expressions:
attrs[ToolControllerTemplate.Expressions] = expressions
return attrs
def execute(self, obj):
Path.Log.track()
args = {
"toolnumber": obj.ToolNumber,
"toollabel": obj.Label,
"spindlespeed": obj.SpindleSpeed,
"spindledirection": SpindleDirection.OFF,
}
if hasattr(obj.Tool, "SpindlePower"):
if not obj.Tool.SpindlePower:
args["spindledirection"] = SpindleDirection.OFF
else:
if obj.SpindleDir == "Forward":
args["spindledirection"] = SpindleDirection.CW
else:
args["spindledirection"] = SpindleDirection.CCW
elif obj.SpindleDir == "None":
args["spindledirection"] = SpindleDirection.OFF
else:
if obj.SpindleDir == "Forward":
args["spindledirection"] = SpindleDirection.CW
else:
args["spindledirection"] = SpindleDirection.CCW
commands = toolchange_generator.generate(**args)
path = Path.Path(commands)
obj.Path = path
if obj.ViewObject:
obj.ViewObject.Visibility = True
def getTool(self, obj):
"""returns the tool associated with this tool controller"""
Path.Log.track()
return obj.Tool
def usesLegacyTool(self, obj):
"""returns True if the tool being controlled is a legacy tool"""
return isinstance(obj.Tool, Path.Tool)
def ensureUseLegacyTool(self, obj, legacy):
if not hasattr(obj, "Tool") or (legacy != self.usesLegacyTool(obj)):
if legacy and hasattr(obj, "Tool") and len(obj.Tool.InList) == 1:
if hasattr(obj.Tool.Proxy, "onDelete"):
obj.Tool.Proxy.onDelete(obj.Tool)
obj.Document.removeObject(obj.Tool.Name)
if hasattr(obj, "Tool"):
obj.removeProperty("Tool")
if legacy:
obj.addProperty(
"Path::PropertyTool",
"Tool",
"Base",
QT_TRANSLATE_NOOP(
"App::Property", "The tool used by this controller"
),
)
else:
obj.addProperty(
"App::PropertyLink",
"Tool",
"Base",
QT_TRANSLATE_NOOP(
"App::Property", "The tool used by this controller"
),
)
def Create(
name="TC: Default Tool",
tool=None,
toolNumber=1,
assignViewProvider=True,
assignTool=True,
):
legacyTool = (
PathPreferences.toolsUseLegacyTools()
if tool is None
else isinstance(tool, Path.Tool)
)
Path.Log.track(tool, toolNumber, legacyTool)
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
obj.Label = name
obj.Proxy = ToolController(obj, legacyTool, assignTool)
if FreeCAD.GuiUp and assignViewProvider:
ViewProvider(obj.ViewObject)
if assignTool:
if not tool:
if legacyTool:
tool = Path.Tool()
tool.Diameter = 5.0
tool.Name = "Default Tool"
tool.CuttingEdgeHeight = 15.0
tool.ToolType = "EndMill"
tool.Material = "HighSpeedSteel"
else:
tool = PathToolBit.Factory.Create()
if tool.ViewObject:
tool.ViewObject.Visibility = False
obj.Tool = tool
if hasattr(obj.Tool, "SpindleDirection"):
obj.SpindleDir = obj.Tool.SpindleDirection
obj.ToolNumber = toolNumber
return obj
def FromTemplate(template, assignViewProvider=True):
Path.Log.track()
name = template.get(ToolControllerTemplate.Name, ToolControllerTemplate.Label)
obj = Create(name, assignViewProvider=True, assignTool=False)
obj.Proxy.setFromTemplate(obj, template)
return obj
if FreeCAD.GuiUp:
# need ViewProvider class in this file to support loading of old files
from PathScripts.PathToolControllerGui import ViewProvider
FreeCAD.Console.PrintLog("Loading PathToolController... done\n")

View File

@@ -1,366 +0,0 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2019 sliptonic <shopinthewoods@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
from PySide import QtCore, QtGui
from PySide.QtCore import QT_TRANSLATE_NOOP
import FreeCAD
import FreeCADGui
import Path
import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts
import PathScripts.PathGui as PathGui
import PathScripts.PathToolBitGui as PathToolBitGui
import PathScripts.PathToolEdit as PathToolEdit
import PathScripts.PathUtil as PathUtil
# lazily loaded modules
from lazy_loader.lazy_loader import LazyLoader
Part = LazyLoader("Part", globals(), "Part")
if False:
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
else:
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
translate = FreeCAD.Qt.translate
class ViewProvider:
def __init__(self, vobj):
vobj.Proxy = self
self.vobj = vobj
def attach(self, vobj):
mode = 2
vobj.setEditorMode("LineWidth", mode)
vobj.setEditorMode("MarkerColor", mode)
vobj.setEditorMode("NormalColor", mode)
vobj.setEditorMode("DisplayMode", mode)
vobj.setEditorMode("BoundingBox", mode)
vobj.setEditorMode("Selectable", mode)
vobj.setEditorMode("ShapeColor", mode)
vobj.setEditorMode("Transparency", mode)
vobj.setEditorMode("Visibility", mode)
self.vobj = vobj
def __getstate__(self):
return None
def __setstate__(self, state):
return None
def getIcon(self):
return ":/icons/Path_ToolController.svg"
def onChanged(self, vobj, prop):
mode = 2
vobj.setEditorMode("LineWidth", mode)
vobj.setEditorMode("MarkerColor", mode)
vobj.setEditorMode("NormalColor", mode)
vobj.setEditorMode("DisplayMode", mode)
vobj.setEditorMode("BoundingBox", mode)
vobj.setEditorMode("Selectable", mode)
def onDelete(self, vobj, args=None):
PathUtil.clearExpressionEngine(vobj.Object)
self.vobj.Object.Proxy.onDelete(vobj.Object, args)
return True
def updateData(self, vobj, prop):
# this is executed when a property of the APP OBJECT changes
pass
def setEdit(self, vobj=None, mode=0):
if 0 == mode:
if vobj is None:
vobj = self.vobj
FreeCADGui.Control.closeDialog()
taskd = TaskPanel(vobj.Object)
FreeCADGui.Control.showDialog(taskd)
taskd.setupUi()
FreeCAD.ActiveDocument.recompute()
return True
return False
def unsetEdit(self, vobj, mode):
# this is executed when the user cancels or terminates edit mode
return False
def setupContextMenu(self, vobj, menu):
Path.Log.track()
for action in menu.actions():
menu.removeAction(action)
action = QtGui.QAction(translate("Path", "Edit"), menu)
action.triggered.connect(self.setEdit)
menu.addAction(action)
def claimChildren(self):
obj = self.vobj.Object
if obj and obj.Proxy and not obj.Proxy.usesLegacyTool(obj):
return [obj.Tool]
return []
def Create(name="Default Tool", tool=None, toolNumber=1):
Path.Log.track(tool, toolNumber)
obj = PathScripts.PathToolController.Create(name, tool, toolNumber)
ViewProvider(obj.ViewObject)
if not obj.Proxy.usesLegacyTool(obj):
# ToolBits are visible by default, which is typically not what the user wants
if tool and tool.ViewObject and tool.ViewObject.Visibility:
tool.ViewObject.Visibility = False
return obj
class CommandPathToolController(object):
def GetResources(self):
return {
"Pixmap": "Path_LengthOffset",
"MenuText": QT_TRANSLATE_NOOP(
"Path_ToolController", "Add Tool Controller to the Job"
),
"ToolTip": QT_TRANSLATE_NOOP("Path_ToolController", "Add Tool Controller"),
}
def selectedJob(self):
if FreeCAD.ActiveDocument:
sel = FreeCADGui.Selection.getSelectionEx()
if sel and sel[0].Object.Name[:3] == "Job":
return sel[0].Object
jobs = [o for o in FreeCAD.ActiveDocument.Objects if o.Name[:3] == "Job"]
if 1 == len(jobs):
return jobs[0]
return None
def IsActive(self):
return self.selectedJob() is not None
def Activated(self):
Path.Log.track()
job = self.selectedJob()
if job:
tool = PathToolBitGui.ToolBitSelector().getTool()
if tool:
toolNr = None
for tc in job.Tools.Group:
if tc.Tool == tool:
toolNr = tc.ToolNumber
break
if not toolNr:
toolNr = max([tc.ToolNumber for tc in job.Tools.Group]) + 1
tc = Create("TC: {}".format(tool.Label), tool, toolNr)
job.Proxy.addToolController(tc)
FreeCAD.ActiveDocument.recompute()
class ToolControllerEditor(object):
def __init__(self, obj, asDialog):
self.form = FreeCADGui.PySideUic.loadUi(":/panels/DlgToolControllerEdit.ui")
if not asDialog:
self.form.buttonBox.hide()
self.obj = obj
comboToPropertyMap = [("spindleDirection", "SpindleDir")]
enumTups = PathScripts.PathToolController.ToolController.propertyEnumerations(
dataType="raw"
)
PathGui.populateCombobox(self.form, enumTups, comboToPropertyMap)
self.vertFeed = PathGui.QuantitySpinBox(self.form.vertFeed, obj, "VertFeed")
self.horizFeed = PathGui.QuantitySpinBox(self.form.horizFeed, obj, "HorizFeed")
self.vertRapid = PathGui.QuantitySpinBox(self.form.vertRapid, obj, "VertRapid")
self.horizRapid = PathGui.QuantitySpinBox(
self.form.horizRapid, obj, "HorizRapid"
)
if obj.Proxy.usesLegacyTool(obj):
self.editor = PathToolEdit.ToolEditor(obj.Tool, self.form.toolEditor)
else:
self.editor = None
self.form.toolBox.widget(1).hide()
self.form.toolBox.removeItem(1)
def selectInComboBox(self, name, combo):
"""selectInComboBox(name, combo) ...
helper function to select a specific value in a combo box."""
blocker = QtCore.QSignalBlocker(combo)
index = combo.currentIndex() # Save initial index
# Search using currentData and return if found
newindex = combo.findData(name)
if newindex >= 0:
combo.setCurrentIndex(newindex)
return
# if not found, search using current text
newindex = combo.findText(name, QtCore.Qt.MatchFixedString)
if newindex >= 0:
combo.setCurrentIndex(newindex)
return
# not found, return unchanged
combo.setCurrentIndex(index)
return
def updateUi(self):
tc = self.obj
self.form.tcName.setText(tc.Label)
self.form.tcNumber.setValue(tc.ToolNumber)
self.horizFeed.updateSpinBox()
self.horizRapid.updateSpinBox()
self.vertFeed.updateSpinBox()
self.vertRapid.updateSpinBox()
self.form.spindleSpeed.setValue(tc.SpindleSpeed)
self.selectInComboBox(tc.SpindleDir, self.form.spindleDirection)
# index = self.form.spindleDirection.findText(
# tc.SpindleDir, QtCore.Qt.MatchFixedString
# )
# if index >= 0:
# self.form.spindleDirection.setCurrentIndex(index)
if self.editor:
self.editor.updateUI()
def updateToolController(self):
tc = self.obj
try:
tc.Label = self.form.tcName.text()
tc.ToolNumber = self.form.tcNumber.value()
self.horizFeed.updateProperty()
self.vertFeed.updateProperty()
self.horizRapid.updateProperty()
self.vertRapid.updateProperty()
tc.SpindleSpeed = self.form.spindleSpeed.value()
tc.SpindleDir = self.form.spindleDirection.currentData()
if self.editor:
self.editor.updateTool()
tc.Tool = self.editor.tool
except Exception as e:
Path.Log.error("Error updating TC: {}".format(e))
def refresh(self):
self.form.blockSignals(True)
self.updateToolController()
self.updateUi()
self.form.blockSignals(False)
def setupUi(self):
if self.editor:
self.editor.setupUI()
self.form.tcName.editingFinished.connect(self.refresh)
self.form.horizFeed.editingFinished.connect(self.refresh)
self.form.vertFeed.editingFinished.connect(self.refresh)
self.form.horizRapid.editingFinished.connect(self.refresh)
self.form.vertRapid.editingFinished.connect(self.refresh)
self.form.spindleSpeed.editingFinished.connect(self.refresh)
self.form.spindleDirection.currentIndexChanged.connect(self.refresh)
class TaskPanel:
def __init__(self, obj):
self.editor = ToolControllerEditor(obj, False)
self.form = self.editor.form
self.updating = False
self.toolrep = None
self.obj = obj
def accept(self):
self.getFields()
FreeCADGui.ActiveDocument.resetEdit()
FreeCADGui.Control.closeDialog()
if self.toolrep:
FreeCAD.ActiveDocument.removeObject(self.toolrep.Name)
FreeCAD.ActiveDocument.recompute()
def reject(self):
FreeCADGui.Control.closeDialog()
if self.toolrep:
FreeCAD.ActiveDocument.removeObject(self.toolrep.Name)
FreeCAD.ActiveDocument.recompute()
def getFields(self):
self.editor.updateToolController()
self.obj.Proxy.execute(self.obj)
def setFields(self):
self.editor.updateUi()
if self.toolrep:
tool = self.obj.Tool
radius = float(tool.Diameter) / 2
length = tool.CuttingEdgeHeight
t = Part.makeCylinder(radius, length)
self.toolrep.Shape = t
def edit(self, item, column):
if not self.updating:
self.resetObject()
def resetObject(self, remove=None):
"transfers the values from the widget to the object"
FreeCAD.ActiveDocument.recompute()
def setupUi(self):
if self.editor.editor:
t = Part.makeCylinder(1, 1)
self.toolrep = FreeCAD.ActiveDocument.addObject("Part::Feature", "tool")
self.toolrep.Shape = t
self.setFields()
self.editor.setupUi()
class DlgToolControllerEdit:
def __init__(self, obj):
self.editor = ToolControllerEditor(obj, True)
self.editor.updateUi()
self.editor.setupUi()
self.obj = obj
def exec_(self):
restoreTC = self.obj.Proxy.templateAttrs(self.obj)
rc = False
if not self.editor.form.exec_():
Path.Log.info("revert")
self.obj.Proxy.setFromTemplate(self.obj, restoreTC)
rc = True
return rc
if FreeCAD.GuiUp:
# register the FreeCAD command
FreeCADGui.addCommand("Path_ToolController", CommandPathToolController())
FreeCAD.Console.PrintLog("Loading PathToolControllerGui... done\n")

View File

@@ -26,9 +26,10 @@ from PySide.QtCore import QT_TRANSLATE_NOOP
import FreeCAD
import FreeCADGui
import Path
import Path.Tools.Controller as PathToolController
import Path.Tools.Gui.BitLibraryCmd as PathToolBitLibraryCmd
import PathScripts
import PathScripts.PathPreferences as PathPreferences
import PathScripts.PathToolBitLibraryCmd as PathToolBitLibraryCmd
import PathScripts.PathToolEdit as PathToolEdit
import PathScripts.PathToolLibraryManager as ToolLibraryManager
import PathScripts.PathUtils as PathUtils
@@ -289,7 +290,7 @@ class EditorPanel:
Path.Log.debug("tool: {}, toolnum: {}".format(tool, toolnum))
if self.job:
label = "T{}: {}".format(toolnum, tool.Name)
tc = PathScripts.PathToolController.Create(
tc = PathToolController.Create(
label, tool=tool, toolNumber=int(toolnum)
)
self.job.Proxy.addToolController(tc)
@@ -300,7 +301,7 @@ class EditorPanel:
and job.Label == targetlist
):
label = "T{}: {}".format(toolnum, tool.Name)
tc = PathScripts.PathToolController.Create(
tc = PathToolController.Create(
label, tool=tool, toolNumber=int(toolnum)
)
job.Proxy.addToolController(tc)

View File

@@ -23,8 +23,8 @@
import FreeCADGui
import FreeCAD
import Path
import Path.Tools.Controller as PathToolsController
import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts
import PathScripts.PathJobCmd as PathJobCmd
import PathScripts.PathUtils as PathUtils
from PySide import QtGui
@@ -46,7 +46,7 @@ class PathUtilsUserInput(object):
for sel in FreeCADGui.Selection.getSelectionEx():
if hasattr(sel.Object, "Proxy"):
if isinstance(
sel.Object.Proxy, PathScripts.PathToolController.ToolController
sel.Object.Proxy, PathToolController.ToolController
):
if tc is None:
tc = sel.Object