Moved Path ToolBit and Controller into Path.Tools module
This commit is contained in:
123
src/Mod/Path/PathScripts/PathDressupDogboneII.py
Normal file
123
src/Mod/Path/PathScripts/PathDressupDogboneII.py
Normal 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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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))
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user