# -*- 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 Path import Path.Base.Util as PathUtil import Path.Base.PropertyBag as PathPropertyBag 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.freecad.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(Path.Preferences.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 Path.Preferences.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 dumps(self): return None def loads(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 try: (doc, docOpened) = self._loadBitBody(obj, path) except FileNotFoundError: Path.Log.error( "Could not find shape file {} for tool bit {}".format(obj.BitShape, obj.Label) ) return 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 attrs["name"] = obj.Label if Path.Preferences.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]) attributes = attrs["attribute"] for att in attributes: PathUtil.setProperty(obj, att, attributes[att]) 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()