Files
create/src/Mod/Path/PathScripts/PathToolBit.py
2021-01-26 18:14:08 -08:00

397 lines
15 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.PathGeom as PathGeom
import PathScripts.PathLog as PathLog
import PathScripts.PathPreferences as PathPreferences
import PathScripts.PathPropertyBag as PathPropertyBag
import PathScripts.PathSetupSheetOpPrototype as PathSetupSheetOpPrototype
import PathScripts.PathUtil as PathUtil
import PySide
import Sketcher
import json
import math
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 _findTool(path, typ, dbg=_DebugFindTool):
PathLog.track(path)
if os.path.exists(path): # absolute reference
if dbg:
PathLog.debug("Found {} at {}".format(typ, path))
return path
def searchFor(pname, fname):
# PathLog.debug("pname: {} fname: {}".format(pname, fname))
if dbg:
PathLog.debug("Looking for {} in {}".format(pname, fname))
if fname:
for p in PathPreferences.searchPathsTool(typ):
PathLog.track(p)
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 os.path.sep != pname:
PathLog.track(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):
PathLog.track(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 _findRelativePath(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):
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'))
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 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)
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():
setattr(attributes, 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):
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 = findShape(p)
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 _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():
# extract property parameters and values so it can be copied
src = attributes.getPropertyByName(prop)
typ = PathPropertyBag.getPropertyType(src)
grp = attributes.getGroupOfProperty(prop)
dsc = attributes.getDocumentationOfProperty(prop)
obj.addProperty(typ, prop, grp, dsc)
obj.setEditorMode(prop, 1)
PathUtil.setProperty(obj, prop, src)
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 = findShape(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'):
PathLog.debug(attrs)
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)
return obj
def CreateFrom(self, path, name='ToolBit'):
PathLog.track(name, path)
try:
data = Declaration(path)
bit = Factory.CreateFromAttrs(data, name)
bit.File = 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):
PathLog.track(name, shapeFile)
obj = FreeCAD.ActiveDocument.addObject('Part::FeaturePython', name)
obj.Proxy = ToolBit(obj, shapeFile)
return obj
Factory = ToolBitFactory()