430 lines
16 KiB
Python
430 lines
16 KiB
Python
# -*- 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 PathScripts.PathLog as PathLog
|
|
import PathScripts.PathPreferences as PathPreferences
|
|
import PathScripts.PathPropertyBag as PathPropertyBag
|
|
import PathScripts.PathUtil as PathUtil
|
|
import PySide
|
|
import json
|
|
import os
|
|
import zipfile
|
|
|
|
# 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
|
|
|
|
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
|
|
#PathLog.trackModule()
|
|
|
|
|
|
def translate(context, text, disambig=None):
|
|
return PySide.QtCore.QCoreApplication.translate(context, text, disambig)
|
|
|
|
def _findToolFile(name, containerFile, typ):
|
|
PathLog.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):
|
|
PathLog.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'''
|
|
PathLog.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'''
|
|
PathLog.track(name, path)
|
|
if name.endswith('.fctb'):
|
|
return _findToolFile(name, path, 'Bit')
|
|
return _findToolFile("{}.fctb".format(name), path, 'Bit')
|
|
|
|
|
|
def findToolLibrary(name, path=None):
|
|
'''findToolLibrary(name, path) ... search for name, if relative path look in path'''
|
|
PathLog.track(name, path)
|
|
if name.endswith('.fctl'):
|
|
return _findToolFile(name, path, 'Library')
|
|
return _findToolFile("{}.fctl".format(name), path, 'Library')
|
|
|
|
|
|
def _findRelativePath(path, typ):
|
|
PathLog.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
|
|
|
|
|
|
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):
|
|
PathLog.track(obj.Label, shapeFile, path)
|
|
self.obj = obj
|
|
obj.addProperty('App::PropertyFile', 'BitShape', 'Base', translate('PathToolBit', 'Shape for bit shape'))
|
|
obj.addProperty('App::PropertyLink', 'BitBody', 'Base', translate('PathToolBit', 'The parametrized body representing the tool bit'))
|
|
obj.addProperty('App::PropertyFile', 'File', 'Base', translate('PathToolBit', 'The file of the tool'))
|
|
obj.addProperty('App::PropertyString', 'ShapeName', 'Base', translate('PathToolBit', 'The name of the shape file'))
|
|
obj.addProperty('App::PropertyStringList', 'BitPropertyNames', 'Base', translate('PathToolBit', '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', translate('PathToolBit', '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):
|
|
PathLog.track(obj.Label, prop)
|
|
if prop == 'BitShape' and 'Restore' not in obj.State:
|
|
self._setupBitShape(obj)
|
|
|
|
def onDelete(self, obj, arg2=None):
|
|
PathLog.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):
|
|
PathLog.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 not path and p != obj.BitShape:
|
|
obj.BitShape = p
|
|
PathLog.debug("ToolBit {} using shape file: {}".format(obj.Label, p))
|
|
doc = FreeCAD.openDocument(p, True)
|
|
obj.ShapeName = doc.Name
|
|
docOpened = True
|
|
else:
|
|
PathLog.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):
|
|
PathLog.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):
|
|
PathLog.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
|
|
|
|
PathLog.debug("bitBody.{} ({}): {}".format(bitBody.Label, bitBody.Name, type(bitBody)))
|
|
|
|
propNames = []
|
|
for attributes in [o for o in bitBody.Group if PathPropertyBag.IsPropertyBag(o)]:
|
|
PathLog.debug("Process properties from {}".format(attributes.Label))
|
|
for prop in attributes.Proxy.getCustomProperties():
|
|
self._setupProperty(obj, prop, attributes)
|
|
propNames.append(prop)
|
|
if not propNames:
|
|
PathLog.error(translate('PathToolBit', '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):
|
|
PathLog.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:
|
|
PathLog.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)
|
|
params = {}
|
|
for name in obj.BitPropertyNames:
|
|
params[name] = PathUtil.getPropertyValueString(obj, name)
|
|
attrs['parameter'] = params
|
|
params = {}
|
|
attrs['attribute'] = params
|
|
return attrs
|
|
|
|
|
|
def Declaration(path):
|
|
PathLog.track(path)
|
|
with open(path, 'r') as fp:
|
|
return json.load(fp)
|
|
|
|
|
|
class ToolBitFactory(object):
|
|
|
|
def CreateFromAttrs(self, attrs, name='ToolBit', path=None):
|
|
PathLog.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'):
|
|
PathLog.track(name, path)
|
|
try:
|
|
data = Declaration(path)
|
|
bit = Factory.CreateFromAttrs(data, name, path)
|
|
return bit
|
|
except (OSError, IOError) as e:
|
|
PathLog.error("%s not a valid tool file (%s)" % (path, e))
|
|
raise
|
|
|
|
def Create(self, name='ToolBit', shapeFile=None, path=None):
|
|
PathLog.track(name, shapeFile, path)
|
|
obj = FreeCAD.ActiveDocument.addObject('Part::FeaturePython', name)
|
|
obj.Proxy = ToolBit(obj, shapeFile, path)
|
|
return obj
|
|
|
|
|
|
Factory = ToolBitFactory()
|