# -*- 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 Part import PathScripts.PathGeom as PathGeom import PathScripts.PathLog as PathLog import PathScripts.PathPreferences as PathPreferences import PathScripts.PathSetupSheetOpPrototype as PathSetupSheetOpPrototype import PathScripts.PathUtil as PathUtil import PySide import Sketcher import json import math import os import zipfile __title__ = "Tool bits." __author__ = "sliptonic (Brad Collette)" __url__ = "http://www.freecadweb.org" __doc__ = "Class to deal with and represent a tool bit." #PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) #PathLog.trackModule() def translate(context, text, disambig=None): return PySide.QtCore.QCoreApplication.translate(context, text, disambig) ParameterTypeConstraint = { 'Angle': 'App::PropertyAngle', 'Distance': 'App::PropertyLength', 'DistanceX': 'App::PropertyLength', 'DistanceY': 'App::PropertyLength', 'Radius': 'App::PropertyLength' } def _findTool(path, typ, dbg=False): if os.path.exists(path): if dbg: PathLog.debug("Found {} at {}".format(typ, path)) return path def searchFor(pname, fname): if dbg: PathLog.debug("Looking for {}".format(pname)) if fname: for p in PathPreferences.searchPathsTool(typ): f = os.path.join(p, fname) if dbg: PathLog.debug(" Checking {}".format(f)) if os.path.exists(f): if dbg: PathLog.debug(" Found {} at {}".format(typ, f)) return f if pname and '/' != pname: ppname, pfname = os.path.split(pname) ffname = os.path.join(pfname, fname) if fname else pfname return searchFor(ppname, ffname) return None return searchFor(path, '') def findShape(path): '''findShape(path) ... search for path, full and partially in all known shape directories.''' return _findTool(path, 'Shape') def findBit(path): if path.endswith('.fctb'): return _findTool(path, 'Bit') return _findTool("{}.fctb".format(path), 'Bit') def findLibrary(path, dbg=False): if path.endswith('.fctl'): return _findTool(path, 'Library', dbg) return _findTool("{}.fctl".format(path), 'Library', dbg) def updateConstraint(sketch, name, value): for i, constraint in enumerate(sketch.Constraints): if constraint.Name.split(';')[0] == name: constr = None if constraint.Type in ['DistanceX', 'DistanceY', 'Distance', 'Radius', 'Angle']: constr = Sketcher.Constraint(constraint.Type, constraint.First, constraint.FirstPos, constraint.Second, constraint.SecondPos, value) else: print(constraint.Name, constraint.Type) if constr is not None: if not PathGeom.isRoughly(constraint.Value, value.Value): PathLog.track(name, constraint.Type, 'update', i, "(%.2f -> %.2f)" % (constraint.Value, value.Value)) sketch.delConstraint(i) sketch.recompute() n = sketch.addConstraint(constr) sketch.renameConstraint(n, constraint.Name) else: PathLog.track(name, constraint.Type, 'unchanged') break PropertyGroupBit = 'Bit' PropertyGroupAttribute = 'Attribute' class ToolBit(object): def __init__(self, obj, shapeFile): PathLog.track(obj.Label, shapeFile) 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')) if shapeFile is not None: 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 propertyNamesBit(self, obj): return [prop for prop in obj.PropertiesList if obj.getGroupOfProperty(prop) == PropertyGroupBit] def propertyNamesAttribute(self, obj): return [prop for prop in obj.PropertiesList if obj.getGroupOfProperty(prop) == PropertyGroupAttribute] def onDocumentRestored(self, obj): obj.setEditorMode('BitShape', 1) obj.setEditorMode('BitBody', 2) obj.setEditorMode('File', 1) obj.setEditorMode('Shape', 2) for prop in self.propertyNamesBit(obj): obj.setEditorMode(prop, 1) # I currently don't see why these need to be read-only #for prop in self.propertyNamesAttribute(obj): # obj.setEditorMode(prop, 1) def onChanged(self, obj, prop): PathLog.track(obj.Label, prop) if prop == 'BitShape' and not 'Restore' in obj.State: self._setupBitShape(obj) #elif obj.getGroupOfProperty(prop) == PropertyGroupBit: # self._updateBitShape(obj, [prop]) def _updateBitShape(self, obj, properties=None): if not obj.BitBody is None: if not properties: properties = self.propertyNamesBit(obj) for prop in properties: for sketch in [o for o in obj.BitBody.Group if o.TypeId == 'Sketcher::SketchObject']: PathLog.track(obj.Label, sketch.Label, prop) updateConstraint(sketch, prop, obj.getPropertyByName(prop)) 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): 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 = findShape(p) if not path and p != obj.BitShape: obj.BitShape = p doc = FreeCAD.open(p) docOpened = True 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 self.propertyNamesBit(obj): obj.removeProperty(prop) def loadBitBody(self, obj, force=False): if force or not obj.BitBody: if force: self._removeBitBody(obj) (doc, opened) = self._loadBitBody(obj) obj.BitBody = obj.Document.copyObject(doc.RootObjects[0], True) if opened: FreeCAD.closeDocument(doc.Name) self._updateBitShape(obj) def unloadBitBody(self, obj): self._removeBitBody(obj) def _setupBitShape(self, obj, path=None): (doc, docOpened) = self._loadBitBody(obj, path) obj.Label = doc.RootObjects[0].Label self._deleteBitSetup(obj) obj.BitBody = obj.Document.copyObject(doc.RootObjects[0], True) if docOpened: FreeCAD.closeDocument(doc.Name) if obj.BitBody.ViewObject: obj.BitBody.ViewObject.Visibility = False self._copyBitShape(obj) for sketch in [o for o in obj.BitBody.Group if o.TypeId == 'Sketcher::SketchObject']: for constraint in [c for c in sketch.Constraints if c.Name != '']: typ = ParameterTypeConstraint.get(constraint.Type) PathLog.track(constraint, typ) if typ is not None: parts = [p.strip() for p in constraint.Name.split(';')] prop = parts[0] desc = '' if len(parts) > 1: desc = parts[1] obj.addProperty(typ, prop, PropertyGroupBit, desc) obj.setEditorMode(prop, 1) value = constraint.Value if constraint.Type == 'Angle': value = value * 180 / math.pi PathUtil.setProperty(obj, prop, value) def getBitThumbnail(self, obj): if obj.BitShape: path = findShape(obj.BitShape) if path: with open(path, 'rb') as fd: zf = zipfile.ZipFile(fd) pf = zf.open('thumbnails/Thumbnail.png', 'r') data = pf.read() pf.close() return data return None def saveToFile(self, obj, path, setFile=True): try: with open(path, 'w') as fp: json.dump(self.shapeAttrs(obj), fp, indent=' ') if setFile: obj.File = path return True except (OSError, IOError) as e: PathLog.error("Could not save tool %s to %s (%s)" % (obj.Label, path, e)) raise def shapeAttrs(self, obj): attrs = {} attrs['version'] = 2 # Path.Tool is version 1 attrs['name'] = obj.Label attrs['shape'] = obj.BitShape params = {} for name in self.propertyNamesBit(obj): params[name] = PathUtil.getPropertyValueString(obj, name) attrs['parameter'] = params params = {} for name in self.propertyNamesAttribute(obj): params[name] = PathUtil.getPropertyValueString(obj, name) attrs['attribute'] = params return attrs def Declaration(path): with open(path, 'r') as fp: return json.load(fp) class AttributePrototype(PathSetupSheetOpPrototype.OpPrototype): def __init__(self): PathSetupSheetOpPrototype.OpPrototype.__init__(self, 'ToolBitAttribute') self.addProperty('App::PropertyEnumeration', 'Material', PropertyGroupAttribute, translate('PathToolBit', 'Tool bit material')) self.Material = ['Carbide', 'CastAlloy', 'Ceramics', 'Diamond', 'HighCarbonToolSteel', 'HighSpeedSteel', 'Sialon'] self.addProperty('App::PropertyDistance', 'LengthOffset', PropertyGroupAttribute, translate('PathToolBit', 'Length offset in Z direction')) self.addProperty('App::PropertyInteger', 'Flutes', PropertyGroupAttribute, translate('PathToolBit', 'The number of flutes')) self.addProperty('App::PropertyDistance', 'ChipLoad', PropertyGroupAttribute, translate('PathToolBit', 'Chipload as per manufacturer')) class ToolBitFactory(object): def CreateFromAttrs(self, attrs, name='ToolBit'): # pylint: disable=protected-access obj = Factory.Create(name, attrs['shape']) 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) params = attrs['attribute'] proto = AttributePrototype() for pname in params: prop = proto.getProperty(pname) val = prop.valueFromString(params[pname]) print("prop[%s] = %s (%s)" % (pname, params[pname], type(val))) prop.setupProperty(obj, pname, PropertyGroupAttribute, prop.valueFromString(params[pname])) return obj def CreateFrom(self, path, name='ToolBit'): try: data = Declaration(path) return Factory.CreateFromAttrs(data, name) except (OSError, IOError) as e: PathLog.error("%s not a valid tool file (%s)" % (path, e)) raise def Create(self, name='ToolBit', shapeFile=None): obj = FreeCAD.ActiveDocument.addObject('Part::FeaturePython', name) obj.Proxy = ToolBit(obj, shapeFile) return obj Factory = ToolBitFactory()