# -*- coding: utf-8 -*- # *************************************************************************** # * Copyright (c) 2019 sliptonic * # * * # * 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()