freecad-cam/Mod/CAM/Path/Tool/Bit.py
2026-02-01 01:59:24 +01:00

501 lines
18 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 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()