Merge pull request #21961 from knipknap/tool-cleanups
CAM: Some cleanups (moving DetachedDocumentObject around)
This commit is contained in:
@@ -1,192 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2025 Samuel Abels <knipknap@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
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
class DetachedDocumentObject:
|
||||
"""
|
||||
A lightweight class mimicking the property API of a FreeCAD DocumentObject.
|
||||
|
||||
This class is used by ToolBit instances when they are not associated
|
||||
with a real FreeCAD DocumentObject, allowing properties to be stored
|
||||
and accessed in a detached state.
|
||||
"""
|
||||
|
||||
def __init__(self, label: str = "DetachedObject"):
|
||||
self.Label: str = label
|
||||
self.Name: str = label.replace(" ", "_")
|
||||
self.PropertiesList: List[str] = []
|
||||
self._properties: Dict[str, Any] = {}
|
||||
self._property_groups: Dict[str, Optional[str]] = {}
|
||||
self._property_types: Dict[str, Optional[str]] = {}
|
||||
self._property_docs: Dict[str, Optional[str]] = {}
|
||||
self._editor_modes: Dict[str, int] = {}
|
||||
self._property_enums: Dict[str, List[str]] = {}
|
||||
|
||||
def addProperty(
|
||||
self,
|
||||
thetype: Optional[str],
|
||||
name: str,
|
||||
group: Optional[str],
|
||||
doc: Optional[str],
|
||||
) -> None:
|
||||
"""Mimics FreeCAD DocumentObject.addProperty."""
|
||||
if name not in self._properties:
|
||||
self.PropertiesList.append(name)
|
||||
self._properties[name] = None
|
||||
self._property_groups[name] = group
|
||||
self._property_types[name] = thetype
|
||||
self._property_docs[name] = doc
|
||||
if thetype in [
|
||||
"App::PropertyQuantity",
|
||||
"App::PropertyLength",
|
||||
"App::PropertyArea",
|
||||
"App::PropertyVolume",
|
||||
"App::PropertyAngle",
|
||||
]:
|
||||
# Initialize Quantity properties with a default value
|
||||
self._properties[name] = FreeCAD.Units.Quantity(0.0)
|
||||
|
||||
def getPropertyByName(self, name: str) -> Any:
|
||||
"""Mimics FreeCAD DocumentObject.getPropertyByName."""
|
||||
return self._properties.get(name)
|
||||
|
||||
def setPropertyByName(self, name: str, value: Any) -> None:
|
||||
"""Mimics FreeCAD DocumentObject.setPropertyByName."""
|
||||
self._properties[name] = value
|
||||
|
||||
def __setattr__(self, name: str, value: Any) -> None:
|
||||
"""
|
||||
Intercept attribute assignment. This is done to behave like
|
||||
FreeCAD's DocumentObject, which may have any property assigned,
|
||||
pre-defined or not.
|
||||
Without this, code linters report an error when trying to set
|
||||
a property that is not defined in the class.
|
||||
|
||||
Handles assignment of enumeration choices (lists/tuples) and
|
||||
converts string representations of Quantity types to Quantity objects.
|
||||
"""
|
||||
if name in ("PropertiesList", "Label", "Name") or name.startswith("_"):
|
||||
super().__setattr__(name, value)
|
||||
return
|
||||
|
||||
# Handle assignment of enumeration choices (list/tuple)
|
||||
prop_type = self._property_types.get(name)
|
||||
if prop_type == "App::PropertyEnumeration" and isinstance(value, (list, tuple)):
|
||||
self._property_enums[name] = list(value)
|
||||
assert len(value) > 0, f"Enum property '{name}' must have at least one entry"
|
||||
self._properties.setdefault(name, value[0])
|
||||
return
|
||||
|
||||
# Attempt to convert string values to Quantity if the property type is Quantity
|
||||
elif prop_type in [
|
||||
"App::PropertyQuantity",
|
||||
"App::PropertyLength",
|
||||
"App::PropertyArea",
|
||||
"App::PropertyVolume",
|
||||
"App::PropertyAngle",
|
||||
]:
|
||||
value = FreeCAD.Units.Quantity(value)
|
||||
|
||||
# Store the (potentially converted) value
|
||||
self._properties[name] = value
|
||||
Path.Log.debug(
|
||||
f"DetachedDocumentObject: Set property '{name}' to "
|
||||
f"value {value} (type: {type(value)})"
|
||||
)
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
"""Intercept attribute access."""
|
||||
if name in self._properties:
|
||||
return self._properties[name]
|
||||
# Default behaviour: raise AttributeError
|
||||
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
|
||||
|
||||
def setEditorMode(self, name: str, mode: int) -> None:
|
||||
"""Stores editor mode settings in detached state."""
|
||||
self._editor_modes[name] = mode
|
||||
|
||||
def getEditorMode(self, name: str) -> int:
|
||||
"""Stores editor mode settings in detached state."""
|
||||
return self._editor_modes.get(name, 0) or 0
|
||||
|
||||
def getGroupOfProperty(self, name: str) -> Optional[str]:
|
||||
"""Returns the stored group for a property in detached state."""
|
||||
return self._property_groups.get(name)
|
||||
|
||||
def getTypeIdOfProperty(self, name: str) -> Optional[str]:
|
||||
"""Returns the stored type string for a property in detached state."""
|
||||
return self._property_types.get(name)
|
||||
|
||||
def getEnumerationsOfProperty(self, name: str) -> List[str]:
|
||||
"""Returns the stored enumeration list for a property."""
|
||||
return self._property_enums.get(name, [])
|
||||
|
||||
@property
|
||||
def ExpressionEngine(self) -> List[Any]:
|
||||
"""Mimics the ExpressionEngine attribute of a real DocumentObject."""
|
||||
return [] # Return an empty list to satisfy iteration
|
||||
|
||||
def copy_to(self, obj: FreeCAD.DocumentObject) -> None:
|
||||
"""
|
||||
Copies properties from this detached object to a real DocumentObject.
|
||||
"""
|
||||
for prop_name in self.PropertiesList:
|
||||
if not hasattr(self, prop_name):
|
||||
continue
|
||||
|
||||
prop_value = self.getPropertyByName(prop_name)
|
||||
prop_type = self._property_types.get(prop_name)
|
||||
prop_group = self._property_groups.get(prop_name)
|
||||
prop_doc = self._property_docs.get(prop_name, "")
|
||||
prop_editor_mode = self._editor_modes.get(prop_name)
|
||||
|
||||
# If the property doesn't exist in the target object, add it
|
||||
if not hasattr(obj, prop_name):
|
||||
# For enums, addProperty expects "App::PropertyEnumeration"
|
||||
# The list of choices is set afterwards.
|
||||
obj.addProperty(prop_type, prop_name, prop_group, prop_doc)
|
||||
|
||||
# If it's an enumeration, set its list of choices first
|
||||
if prop_type == "App::PropertyEnumeration":
|
||||
enum_choices = self._property_enums.get(prop_name)
|
||||
assert enum_choices is not None
|
||||
setattr(obj, prop_name, enum_choices)
|
||||
|
||||
# Set the property value and editor mode
|
||||
try:
|
||||
if prop_type == "App::PropertyEnumeration":
|
||||
first_choice = self._property_enums[prop_name][0]
|
||||
setattr(obj, prop_name, first_choice)
|
||||
setattr(obj, prop_name, prop_value)
|
||||
|
||||
except Exception as e:
|
||||
Path.Log.error(
|
||||
f"Error setting property {prop_name} to {prop_value} "
|
||||
f"(type: {type(prop_value)}, expected type: {prop_type}): {e}"
|
||||
)
|
||||
raise
|
||||
|
||||
if prop_editor_mode is not None:
|
||||
obj.setEditorMode(prop_name, prop_editor_mode)
|
||||
@@ -33,10 +33,10 @@ from lazy_loader.lazy_loader import LazyLoader
|
||||
from typing import Any, List, Optional, Tuple, Type, Union, Mapping, cast
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
from Path.Base.Generator import toolchange
|
||||
from ...assets import Asset
|
||||
from ...docobject import DetachedDocumentObject
|
||||
from ...assets.asset import Asset
|
||||
from ...camassets import cam_assets
|
||||
from ...shape import ToolBitShape, ToolBitShapeCustom, ToolBitShapeIcon
|
||||
from ..docobject import DetachedDocumentObject
|
||||
from ..util import to_json, format_value
|
||||
|
||||
|
||||
|
||||
@@ -22,13 +22,12 @@
|
||||
|
||||
"""Widget for editing a ToolBit object."""
|
||||
|
||||
from functools import partial
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
from PySide import QtGui, QtCore
|
||||
from ..models.base import ToolBit
|
||||
from ...shape.ui.shapewidget import ShapeWidget
|
||||
from ...ui.docobject import DocumentObjectEditorWidget
|
||||
from ...docobject.ui import DocumentObjectEditorWidget
|
||||
from ..models.base import ToolBit
|
||||
|
||||
|
||||
class ToolBitPropertiesWidget(QtGui.QWidget):
|
||||
|
||||
Reference in New Issue
Block a user