CAM: Refactor shape type handling for aliases and subtypes
This PR intruduces a way to dynamicly alias toolbits and adds subtype for toolbits. Prior to this change, aliases were hardcoded in multiple locations, making it difficult to maintain and extend. Now, aliases and the newly subtypes are defined in a single JSON file, which is loaded at runtime. This allows for easier management and addition of new aliases or subtypes without modifying the codebase. Updated the shape system to use a dict format for aliases and subtypes, improved label formatting, and fixed normalization logic for toolbit types. Enhanced dropdown display and editor logic to properly handle subtypes and aliases, including special cases for consecutive capitals. Users can add shape_aliases.json to the toolbit Shape dirctory allowing theme to add/remove entries. Adding a "!" in front of a alias or a subtime removes it from the list. This PR also introduces the ability for the user to choose tools from a libary or all tools when adding tools via the Job panel. src/Mod/CAM/Path/Tool/docobject/ui/docobject.py: - Added keep_case and preserve_consecutive_caps options to _get_label_text for improved label formatting src/Mod/CAM/Path/Tool/shape/__init__.py: - Refactored alias/subtype loading to support new dict format - Applied aliases and subtypes to shape classes src/Mod/CAM/Path/Tool/shape/models/base.py: - Added subtypes attribute to ToolBitShape - Updated class lookup to support subtypes src/Mod/CAM/Path/Tool/toolbit/models/base.py: - Improved normalization logic for aliases and subtypes in ToolBit.from_dict src/Mod/CAM/Path/Tool/toolbit/ui/editor.py: - Updated editor logic to use subtypes and aliases for dropdowns - Improved read-only and editable state handling src/Mod/CAM/Path/Tool/toolbit/ui/typefilter.py: - Used _get_label_text for dropdown filter display with improved formatting src/Mod/CAM/Tools/Shape/shape_aliases.json: - Converted to new dict format with separate aliases and subtypes
This commit is contained in:
@@ -235,6 +235,7 @@ SET(PathPythonToolsToolBitUi_SRCS
|
||||
Path/Tool/toolbit/ui/toollist.py
|
||||
Path/Tool/toolbit/ui/util.py
|
||||
Path/Tool/toolbit/ui/view.py
|
||||
Path/Tool/toolbit/ui/typefilter.py
|
||||
)
|
||||
|
||||
SET(PathPythonToolsLibrary_SRCS
|
||||
@@ -496,6 +497,7 @@ SET(Tools_Shape_SRCS
|
||||
Tools/Shape/thread-mill.svg
|
||||
Tools/Shape/v-bit.fcstd
|
||||
Tools/Shape/v-bit.svg
|
||||
Tools/Shape/shape_aliases.json
|
||||
)
|
||||
|
||||
SET(Tests_SRCS
|
||||
|
||||
@@ -1143,16 +1143,31 @@ class TaskPanel:
|
||||
self.toolControllerSelect()
|
||||
|
||||
def toolControllerAdd(self):
|
||||
selector = ToolBitSelector(compact=True)
|
||||
selector = ToolBitSelector(compact=True, show_all_tools=True)
|
||||
if not selector.exec_():
|
||||
return
|
||||
toolbit = selector.get_selected_tool()
|
||||
toolbit.attach_to_doc(FreeCAD.ActiveDocument)
|
||||
toolNum = self.obj.Proxy.nextToolNumber()
|
||||
tc = PathToolControllerGui.Create(
|
||||
name=f"TC: {toolbit.label}", tool=toolbit.obj, toolNumber=toolNum
|
||||
)
|
||||
self.obj.Proxy.addToolController(tc)
|
||||
|
||||
toolbits = selector.get_selected_tools()
|
||||
if not toolbits:
|
||||
return
|
||||
|
||||
# Get tool numbers mapping (from library or empty for auto-increment)
|
||||
tool_numbers = selector.get_tool_numbers()
|
||||
|
||||
# Add each selected tool
|
||||
for toolbit in toolbits:
|
||||
toolbit.attach_to_doc(FreeCAD.ActiveDocument)
|
||||
|
||||
# Get tool number: use library number if available, otherwise auto-increment
|
||||
toolbit_uri = str(toolbit.get_uri())
|
||||
toolNum = tool_numbers.get(toolbit_uri)
|
||||
if toolNum is None:
|
||||
toolNum = self.obj.Proxy.nextToolNumber()
|
||||
|
||||
tc = PathToolControllerGui.Create(
|
||||
name=f"TC: {toolbit.label}", tool=toolbit.obj, toolNumber=toolNum
|
||||
)
|
||||
self.obj.Proxy.addToolController(tc)
|
||||
|
||||
FreeCAD.ActiveDocument.recompute()
|
||||
self.updateToolController()
|
||||
|
||||
@@ -28,16 +28,24 @@ from PySide import QtGui, QtCore
|
||||
from .property import BasePropertyEditorWidget
|
||||
|
||||
|
||||
def _get_label_text(prop_name):
|
||||
def _get_label_text(prop_name, keep_case=False, preserve_consecutive_caps=False):
|
||||
"""Generate a human-readable label from a property name."""
|
||||
# Add space before capital letters (CamelCase splitting)
|
||||
s1 = re.sub(r"([A-Z][a-z]+)", r" \1", prop_name)
|
||||
# Add space before sequences of capitals (e.g., ID) followed by lowercase
|
||||
s2 = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1 \2", s1)
|
||||
if preserve_consecutive_caps:
|
||||
s1 = re.sub(r"(?<![A-Z])([A-Z][a-z]+)", r" \1", prop_name)
|
||||
# Skip splitting short capital sequences (e.g., VBit stays VBit)
|
||||
s2 = re.sub(r"([A-Z]{3,})([A-Z][a-z])", r"\1 \2", s1)
|
||||
else:
|
||||
s1 = re.sub(r"([A-Z][a-z]+)", r" \1", prop_name)
|
||||
# Add space before sequences of capitals (e.g., ID) followed by lowercase
|
||||
s2 = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1 \2", s1)
|
||||
# Add space before sequences of capitals followed by end of string
|
||||
s3 = re.sub(r"([A-Z]+)$", r" \1", s2)
|
||||
# Remove leading/trailing spaces and capitalize
|
||||
return s3.strip().capitalize()
|
||||
# Remove leading/trailing spaces
|
||||
result = s3.strip()
|
||||
if not keep_case:
|
||||
return result.capitalize()
|
||||
return result.title()
|
||||
|
||||
|
||||
class DocumentObjectEditorWidget(QtGui.QWidget):
|
||||
|
||||
@@ -35,6 +35,7 @@ from ...toolbit import ToolBit
|
||||
from ...toolbit.ui import ToolBitEditor
|
||||
from ...toolbit.ui.util import natural_sort_key
|
||||
from ...toolbit.ui.browser import ToolBitBrowserWidget, ToolBitUriRole
|
||||
from ...toolbit.ui.typefilter import ToolBitTypeFilterMixin
|
||||
from ...toolbit.serializers import YamlToolBitSerializer
|
||||
from ..models.library import Library
|
||||
|
||||
@@ -43,7 +44,7 @@ Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
|
||||
Path.Log.trackModule(Path.Log.thisModule())
|
||||
|
||||
|
||||
class LibraryBrowserWidget(ToolBitBrowserWidget):
|
||||
class LibraryBrowserWidget(ToolBitBrowserWidget, ToolBitTypeFilterMixin):
|
||||
"""
|
||||
A widget to browse, filter, and select Tool Library assets from the
|
||||
AssetManager, with sorting and batch insertion, using a current library.
|
||||
@@ -57,6 +58,7 @@ class LibraryBrowserWidget(ToolBitBrowserWidget):
|
||||
store: str = "local",
|
||||
parent=None,
|
||||
compact=True,
|
||||
show_all_tools=False,
|
||||
):
|
||||
super().__init__(
|
||||
asset_manager=asset_manager,
|
||||
@@ -67,6 +69,7 @@ class LibraryBrowserWidget(ToolBitBrowserWidget):
|
||||
)
|
||||
self.current_library: Optional[Library] = None
|
||||
self._selected_tool_type: Optional[str] = None
|
||||
self._show_all_tools = show_all_tools
|
||||
self.layout().setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# Add tool type filter combo box to the base widget
|
||||
@@ -166,7 +169,7 @@ class LibraryBrowserWidget(ToolBitBrowserWidget):
|
||||
except FileNotFoundError:
|
||||
Path.Log.error(f"Library {library_uri} not found.")
|
||||
self.current_library = None
|
||||
self._update_tool_list()
|
||||
self._reload_assets_from_library()
|
||||
|
||||
def get_tool_no_from_current_library(self, toolbit):
|
||||
"""
|
||||
@@ -180,79 +183,39 @@ class LibraryBrowserWidget(ToolBitBrowserWidget):
|
||||
def set_current_library(self, library):
|
||||
"""Sets the current library and updates the tool list."""
|
||||
self.current_library = library
|
||||
self._update_tool_list()
|
||||
self._reload_assets_from_library()
|
||||
self.current_library_changed.emit()
|
||||
|
||||
# Save the selected library to preferences
|
||||
if library:
|
||||
Path.Preferences.setLastToolLibrary(str(library.get_uri()))
|
||||
|
||||
def _get_available_tool_types(self):
|
||||
"""Get all available tool types from the current assets."""
|
||||
tool_types = set()
|
||||
# Make sure we have assets to work with
|
||||
if not hasattr(self, "_all_assets") or not self._all_assets:
|
||||
return []
|
||||
def _get_assets_for_type_filter(self):
|
||||
"""Returns the list of assets to use for type filtering."""
|
||||
return getattr(self, "_all_assets", [])
|
||||
|
||||
for asset in self._all_assets:
|
||||
# Use get_shape_name() method to get the tool type
|
||||
if hasattr(asset, "get_shape_name"):
|
||||
tool_type = asset.get_shape_name()
|
||||
if tool_type:
|
||||
tool_types.add(tool_type)
|
||||
def _on_type_filter_changed(self):
|
||||
"""Called when the type filter changes - updates the list."""
|
||||
self._refresh_filtered_list(self._tool_list_widget, self._search_edit, self._all_assets)
|
||||
|
||||
return sorted(tool_types)
|
||||
|
||||
def _get_filtered_assets(self):
|
||||
"""Get assets filtered by tool type if a specific type is selected."""
|
||||
if self._tool_type_combo.currentIndex() == 0: # "All Toolbit Types"
|
||||
return self._all_assets
|
||||
|
||||
filtered_assets = []
|
||||
for asset in self._all_assets:
|
||||
if hasattr(asset, "get_shape_name"):
|
||||
tool_type = asset.get_shape_name()
|
||||
if tool_type == self._selected_tool_type:
|
||||
filtered_assets.append(asset)
|
||||
return filtered_assets
|
||||
|
||||
def _update_tool_list(self):
|
||||
"""Updates the tool list based on the current library."""
|
||||
def _reload_assets_from_library(self):
|
||||
"""Reloads assets from the current library and refreshes the UI."""
|
||||
if self.current_library:
|
||||
self._all_assets = [t for t in self.current_library]
|
||||
else:
|
||||
# Fetch all toolbits
|
||||
# Fetch all toolbits (when no library selected or "All Tools" selected)
|
||||
all_toolbits = self._asset_manager.fetch(asset_type="toolbit", depth=0)
|
||||
self._all_assets = cast(List[ToolBit], all_toolbits)
|
||||
self._sort_assets()
|
||||
self._tool_list_widget.clear_list()
|
||||
# Update tool type combo after assets are loaded
|
||||
if hasattr(self, "_tool_type_combo"):
|
||||
self._update_tool_type_combo()
|
||||
self._update_list()
|
||||
self._refresh_filtered_list(self._tool_list_widget, self._search_edit, self._all_assets)
|
||||
|
||||
def _update_list(self):
|
||||
"""Updates the list widget with filtered assets."""
|
||||
self._tool_list_widget.clear_list()
|
||||
filtered_assets = self._get_filtered_assets()
|
||||
|
||||
# Apply search filter if there is one
|
||||
search_term = self._search_edit.text().lower()
|
||||
if search_term:
|
||||
search_filtered = []
|
||||
for asset in filtered_assets:
|
||||
if search_term in asset.label.lower():
|
||||
search_filtered.append(asset)
|
||||
continue
|
||||
# Also search in tool type
|
||||
if hasattr(asset, "get_shape_name"):
|
||||
tool_type = asset.get_shape_name()
|
||||
if tool_type and search_term in tool_type.lower():
|
||||
search_filtered.append(asset)
|
||||
filtered_assets = search_filtered
|
||||
|
||||
for asset in filtered_assets:
|
||||
self._tool_list_widget.add_toolbit(asset)
|
||||
"""Override base class to prevent search highlighting."""
|
||||
super()._update_list()
|
||||
# Remove any highlighting by passing empty string
|
||||
|
||||
def _add_shortcuts(self):
|
||||
"""Adds keyboard shortcuts for common actions."""
|
||||
@@ -356,7 +319,6 @@ class LibraryBrowserWidget(ToolBitBrowserWidget):
|
||||
|
||||
state = self._get_state()
|
||||
self.refresh()
|
||||
self._update_list()
|
||||
self._set_state(state)
|
||||
|
||||
def _on_cut_requested(self):
|
||||
@@ -571,31 +533,6 @@ class LibraryBrowserWidget(ToolBitBrowserWidget):
|
||||
self._asset_manager.add(library)
|
||||
self.refresh()
|
||||
|
||||
def _update_tool_type_combo(self):
|
||||
"""Update the tool type combo box with available types."""
|
||||
current_selection = self._tool_type_combo.currentText()
|
||||
self._tool_type_combo.blockSignals(True)
|
||||
try:
|
||||
self._tool_type_combo.clear()
|
||||
self._tool_type_combo.addItem(FreeCAD.Qt.translate("CAM", "All Toolbit Types"))
|
||||
|
||||
for tool_type in self._get_available_tool_types():
|
||||
self._tool_type_combo.addItem(tool_type)
|
||||
|
||||
# Restore selection if it still exists
|
||||
index = self._tool_type_combo.findText(current_selection)
|
||||
if index >= 0:
|
||||
self._tool_type_combo.setCurrentIndex(index)
|
||||
else:
|
||||
self._tool_type_combo.setCurrentIndex(0)
|
||||
finally:
|
||||
self._tool_type_combo.blockSignals(False)
|
||||
|
||||
def _on_tool_type_combo_changed(self, tool_type):
|
||||
"""Handle tool type filter selection change."""
|
||||
self._selected_tool_type = tool_type
|
||||
self._update_list()
|
||||
|
||||
|
||||
class LibraryBrowserWithCombo(LibraryBrowserWidget):
|
||||
"""
|
||||
@@ -608,6 +545,7 @@ class LibraryBrowserWithCombo(LibraryBrowserWidget):
|
||||
store: str = "local",
|
||||
parent=None,
|
||||
compact=True,
|
||||
show_all_tools=False,
|
||||
):
|
||||
super().__init__(
|
||||
asset_manager=asset_manager,
|
||||
@@ -616,6 +554,8 @@ class LibraryBrowserWithCombo(LibraryBrowserWidget):
|
||||
compact=compact,
|
||||
)
|
||||
|
||||
self._show_all_tools = show_all_tools
|
||||
|
||||
# Move search box into dedicated row to make space for the
|
||||
# library selection combo box
|
||||
layout = self.layout()
|
||||
@@ -642,7 +582,10 @@ class LibraryBrowserWithCombo(LibraryBrowserWidget):
|
||||
return
|
||||
|
||||
selected_library = cast(Library, self._library_combo.itemData(index))
|
||||
|
||||
# Handle "All Tools" selection (userData is None)
|
||||
if not selected_library:
|
||||
self.set_current_library(None)
|
||||
return
|
||||
|
||||
# Have to refetch the non-shallow library.
|
||||
@@ -655,12 +598,16 @@ class LibraryBrowserWithCombo(LibraryBrowserWidget):
|
||||
if self.current_library:
|
||||
for i in range(self._library_combo.count()):
|
||||
lib = self._library_combo.itemData(i)
|
||||
if lib.get_uri() == self.current_library.get_uri():
|
||||
if lib and lib.get_uri() == self.current_library.get_uri():
|
||||
self._library_combo.setCurrentIndex(i)
|
||||
return
|
||||
Path.Log.warning(
|
||||
f"Current library {self.current_library.get_uri()} not found in combo box."
|
||||
)
|
||||
else:
|
||||
# current_library is None, select "All Tools" if available
|
||||
if self._show_all_tools:
|
||||
self._library_combo.setCurrentIndex(0)
|
||||
|
||||
def refresh(self):
|
||||
"""Reads available libraries and refreshes the combo box and toolbits."""
|
||||
@@ -669,6 +616,11 @@ class LibraryBrowserWithCombo(LibraryBrowserWidget):
|
||||
self._in_refresh = True
|
||||
try:
|
||||
self._library_combo.clear()
|
||||
|
||||
# Add "All Tools" option if requested
|
||||
if self._show_all_tools:
|
||||
self._library_combo.addItem(FreeCAD.Qt.translate("CAM", "All Tools"), userData=None)
|
||||
|
||||
for library in sorted(libraries, key=lambda x: natural_sort_key(x.label)):
|
||||
self._library_combo.addItem(library.label, userData=library)
|
||||
finally:
|
||||
@@ -689,7 +641,7 @@ class LibraryBrowserWithCombo(LibraryBrowserWidget):
|
||||
|
||||
for i in range(self._library_combo.count()):
|
||||
lib = self._library_combo.itemData(i)
|
||||
if lib.get_uri() == self.current_library.get_uri():
|
||||
if lib and self.current_library and lib.get_uri() == self.current_library.get_uri():
|
||||
self._library_combo.setCurrentIndex(i)
|
||||
break
|
||||
else:
|
||||
|
||||
@@ -505,13 +505,25 @@ class LibraryEditor(QWidget):
|
||||
new_toolbit = tool_bit_class(shape)
|
||||
new_toolbit.label = FreeCAD.Qt.translate("CAM", "New Toolbit")
|
||||
|
||||
# Save the individual toolbit asset first
|
||||
# Get next available tool number for the library
|
||||
tool_no = None
|
||||
if current_library:
|
||||
tool_no = current_library.get_next_bit_no()
|
||||
|
||||
editor = ToolBitEditor(new_toolbit, tool_no=tool_no)
|
||||
result = editor.show()
|
||||
if result != QDialog.Accepted:
|
||||
return
|
||||
|
||||
if current_library:
|
||||
tool_no = editor.get_tool_no()
|
||||
|
||||
tool_asset_uri = cam_assets.add(new_toolbit)
|
||||
Path.Log.debug(f"_on_add_toolbit_requested: Saved tool with URI: {tool_asset_uri}")
|
||||
|
||||
# Add the toolbit to the current library if one is selected
|
||||
if current_library:
|
||||
toolno = current_library.add_bit(new_toolbit)
|
||||
toolno = current_library.add_bit(new_toolbit, bit_no=tool_no)
|
||||
Path.Log.debug(
|
||||
f"_on_add_toolbit_requested: Added toolbit {new_toolbit.get_id()} (URI: {new_toolbit.get_uri()}) "
|
||||
f"to current_library with number {toolno}."
|
||||
@@ -534,6 +546,23 @@ class LibraryEditor(QWidget):
|
||||
|
||||
setToolBitSchema() # Ensure correct schema is set for the new toolbit
|
||||
self.browser.refresh()
|
||||
|
||||
# Set the filter dropdown to show the new toolbit's type
|
||||
subtype = new_toolbit.get_subtype()
|
||||
if subtype:
|
||||
# If it has a subtype (e.g., "compression", "variable_flute"), select that
|
||||
filter_value = subtype
|
||||
else:
|
||||
# Otherwise select the parent shape type (e.g., "Endmill", "Probe")
|
||||
filter_value = new_toolbit.get_shape_name()
|
||||
|
||||
# Find and set the filter dropdown by matching the data value
|
||||
if hasattr(self.browser, "_tool_type_combo"):
|
||||
for i in range(self.browser._tool_type_combo.count()):
|
||||
if self.browser._tool_type_combo.itemData(i) == filter_value:
|
||||
self.browser._tool_type_combo.setCurrentIndex(i)
|
||||
break
|
||||
|
||||
self.browser.select_by_uri([str(new_toolbit.get_uri())])
|
||||
self._update_button_states()
|
||||
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
# This package aggregates tool bit shape classes.
|
||||
|
||||
import os
|
||||
import json
|
||||
import Path
|
||||
from Path.Preferences import getAssetPath, getBuiltinAssetPath
|
||||
|
||||
# Import the base class and all concrete shape classes
|
||||
from .models.base import ToolBitShape
|
||||
from .models.ballend import ToolBitShapeBallend
|
||||
@@ -25,6 +30,99 @@ from .models.icon import (
|
||||
ToolBitShapeSvgIcon,
|
||||
)
|
||||
|
||||
# Paths for alias files
|
||||
_INTERNAL_ALIASES_FILE = os.path.join(getBuiltinAssetPath(), "Shape", "shape_aliases.json")
|
||||
_USER_ALIASES_FILE = os.path.join(getAssetPath(), "Tools", "Shape", "shape_aliases.json")
|
||||
|
||||
|
||||
def _load_shape_aliases():
|
||||
"""Load shape aliases from JSON files (internal default merged with user overrides, avoiding duplicates)."""
|
||||
aliases = {}
|
||||
|
||||
# Always load internal file first
|
||||
try:
|
||||
if os.path.isfile(_INTERNAL_ALIASES_FILE):
|
||||
with open(_INTERNAL_ALIASES_FILE, "r") as f:
|
||||
aliases = json.load(f)
|
||||
Path.Log.debug(f"Loaded default shape aliases from {_INTERNAL_ALIASES_FILE}")
|
||||
else:
|
||||
Path.Log.warning(
|
||||
f"Internal shape aliases file not found: {_INTERNAL_ALIASES_FILE}"
|
||||
) # Should not happen in installed system
|
||||
except Exception as e:
|
||||
Path.Log.error(f"Failed to load internal shape aliases: {e}")
|
||||
|
||||
# Merge user file if it exists
|
||||
try:
|
||||
if os.path.isfile(_USER_ALIASES_FILE):
|
||||
with open(_USER_ALIASES_FILE, "r") as f:
|
||||
user_aliases = json.load(f)
|
||||
|
||||
# Merge user aliases into the loaded aliases
|
||||
for shape_name, data in user_aliases.items():
|
||||
if shape_name not in aliases:
|
||||
aliases[shape_name] = {"aliases": [], "subtypes": []}
|
||||
|
||||
if "aliases" in data:
|
||||
# Use set to avoid duplicates
|
||||
existing_aliases = set(aliases[shape_name]["aliases"])
|
||||
for alias in data["aliases"]:
|
||||
if alias.startswith("!"):
|
||||
# Remove alias (strip the ! prefix)
|
||||
existing_aliases.discard(alias[1:])
|
||||
else:
|
||||
# Add alias
|
||||
existing_aliases.add(alias)
|
||||
aliases[shape_name]["aliases"] = sorted(existing_aliases)
|
||||
if "subtypes" in data:
|
||||
# Use set to avoid duplicates
|
||||
existing_subtypes = set(aliases[shape_name]["subtypes"])
|
||||
for subtype in data["subtypes"]:
|
||||
if subtype.startswith("!"):
|
||||
# Remove subtype (strip the ! prefix)
|
||||
existing_subtypes.discard(subtype[1:])
|
||||
else:
|
||||
# Add subtype
|
||||
existing_subtypes.add(subtype)
|
||||
aliases[shape_name]["subtypes"] = sorted(existing_subtypes)
|
||||
|
||||
Path.Log.info(f"Merged user shape aliases from {_USER_ALIASES_FILE}")
|
||||
except Exception as e:
|
||||
Path.Log.error(f"Failed to load/merge user shape aliases: {e}")
|
||||
|
||||
return aliases
|
||||
|
||||
|
||||
def _apply_aliases_to_shape_classes():
|
||||
"""Apply loaded aliases and subtypes to all registered shape classes."""
|
||||
config = _load_shape_aliases()
|
||||
|
||||
# Map class names to actual classes
|
||||
shape_classes = {cls.name: cls for cls in ToolBitShape.__subclasses__()}
|
||||
|
||||
for shape_name, data in config.items():
|
||||
if shape_name not in shape_classes:
|
||||
Path.Log.warning(f"Unknown shape class '{shape_name}' in aliases file")
|
||||
continue
|
||||
|
||||
shape_class = shape_classes[shape_name]
|
||||
|
||||
# Expect dict format with aliases/subtypes
|
||||
if not isinstance(data, dict):
|
||||
Path.Log.warning(f"Invalid data format for {shape_name} - expected dict")
|
||||
continue
|
||||
|
||||
aliases_list = data.get("aliases", [])
|
||||
subtypes_list = data.get("subtypes", [])
|
||||
shape_class.aliases = tuple(sorted(alias.lower() for alias in aliases_list))
|
||||
shape_class.subtypes = tuple(sorted(subtype.lower() for subtype in subtypes_list))
|
||||
Path.Log.info(f"Applied aliases to {shape_name}: {aliases_list}")
|
||||
Path.Log.info(f"Applied subtypes to {shape_name}: {subtypes_list}")
|
||||
|
||||
|
||||
# Load and apply aliases immediately after imports
|
||||
_apply_aliases_to_shape_classes()
|
||||
|
||||
# A list of the name of each ToolBitShape
|
||||
TOOL_BIT_SHAPE_NAMES = sorted([cls.name for cls in ToolBitShape.__subclasses__()])
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ from .base import ToolBitShape
|
||||
|
||||
class ToolBitShapeBallend(ToolBitShape):
|
||||
name: str = "Ballend"
|
||||
aliases = ("ballend",)
|
||||
|
||||
@classmethod
|
||||
def schema(cls) -> Mapping[str, Tuple[str, str]]:
|
||||
|
||||
@@ -68,6 +68,11 @@ class ToolBitShape(Asset):
|
||||
# "v-bit", "vbit", etc.
|
||||
aliases: Tuple[str, ...] = tuple()
|
||||
|
||||
# Subtypes are shape variants (e.g., "roughing" for endmill).
|
||||
# Unlike aliases, subtypes preserve their identity and don't normalize.
|
||||
# NOTE: Subtypes are set dynamically from the shape_aliases.json file.
|
||||
subtypes: Tuple[str, ...] = tuple()
|
||||
|
||||
def __init__(self, id: str, **kwargs: Any):
|
||||
"""
|
||||
Initialize the shape.
|
||||
@@ -501,7 +506,8 @@ class ToolBitShape(Asset):
|
||||
cls, name: str, default: Optional[Type["ToolBitShape"]] = None
|
||||
) -> Optional[Type["ToolBitShape"]]:
|
||||
"""
|
||||
Retrieves a ToolBitShape class by its name or alias.
|
||||
Retrieves a ToolBitShape class by its name, alias, or subtype.
|
||||
Note: Both aliases and subtypes return the parent class.
|
||||
"""
|
||||
name = name.lower()
|
||||
for thecls in cls.__subclasses__():
|
||||
@@ -509,6 +515,7 @@ class ToolBitShape(Asset):
|
||||
thecls.name.lower() == name
|
||||
or thecls.__name__.lower() == name
|
||||
or name in thecls.aliases
|
||||
or name in thecls.subtypes
|
||||
):
|
||||
return thecls
|
||||
return default
|
||||
@@ -518,7 +525,7 @@ class ToolBitShape(Asset):
|
||||
cls, name: str, default: Optional[Type["ToolBitShape"]] = None
|
||||
) -> Optional[Type["ToolBitShape"]]:
|
||||
"""
|
||||
Retrieves a ToolBitShape class by its name or alias.
|
||||
Retrieves a ToolBitShape class by its name, alias, or subtype.
|
||||
"""
|
||||
name = name.lower()
|
||||
for thecls in cls.__subclasses__():
|
||||
@@ -527,6 +534,9 @@ class ToolBitShape(Asset):
|
||||
for alias in thecls.aliases:
|
||||
if alias.lower() in name:
|
||||
return thecls
|
||||
for subtype in thecls.subtypes:
|
||||
if subtype.lower() in name:
|
||||
return thecls
|
||||
return default
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -36,7 +36,6 @@ class ToolBitShapeBullnose(ToolBitShape):
|
||||
return params
|
||||
|
||||
name = "Bullnose"
|
||||
aliases = "bullnose", "torus"
|
||||
|
||||
@classmethod
|
||||
def schema(cls) -> Mapping[str, Tuple[str, str]]:
|
||||
|
||||
@@ -28,7 +28,6 @@ from .base import ToolBitShape
|
||||
|
||||
class ToolBitShapeChamfer(ToolBitShape):
|
||||
name = "Chamfer"
|
||||
aliases = ("chamfer",)
|
||||
|
||||
@classmethod
|
||||
def schema(cls) -> Mapping[str, Tuple[str, str]]:
|
||||
|
||||
@@ -33,7 +33,6 @@ class ToolBitShapeCustom(ToolBitShape):
|
||||
"""
|
||||
|
||||
name: str = "Custom"
|
||||
aliases = ("custom",)
|
||||
|
||||
# Connor: We're going to treat custom tools as normal endmills
|
||||
@classmethod
|
||||
|
||||
@@ -28,7 +28,6 @@ from .base import ToolBitShape
|
||||
|
||||
class ToolBitShapeDovetail(ToolBitShape):
|
||||
name = "Dovetail"
|
||||
aliases = ("dovetail",)
|
||||
|
||||
@classmethod
|
||||
def schema(cls) -> Mapping[str, Tuple[str, str]]:
|
||||
|
||||
@@ -28,7 +28,6 @@ from .base import ToolBitShape
|
||||
|
||||
class ToolBitShapeDrill(ToolBitShape):
|
||||
name = "Drill"
|
||||
aliases = ("drill",)
|
||||
|
||||
@classmethod
|
||||
def schema(cls) -> Mapping[str, Tuple[str, str]]:
|
||||
|
||||
@@ -28,7 +28,6 @@ from .base import ToolBitShape
|
||||
|
||||
class ToolBitShapeEndmill(ToolBitShape):
|
||||
name = "Endmill"
|
||||
aliases = ("endmill",)
|
||||
|
||||
@classmethod
|
||||
def schema(cls) -> Mapping[str, Tuple[str, str]]:
|
||||
|
||||
@@ -28,7 +28,6 @@ from .base import ToolBitShape
|
||||
|
||||
class ToolBitShapeProbe(ToolBitShape):
|
||||
name = "Probe"
|
||||
aliases = ("probe",)
|
||||
|
||||
@classmethod
|
||||
def schema(cls) -> Mapping[str, Tuple[str, str]]:
|
||||
|
||||
@@ -28,10 +28,6 @@ from .base import ToolBitShape
|
||||
|
||||
class ToolBitShapeRadius(ToolBitShape):
|
||||
name = "Radius"
|
||||
aliases = (
|
||||
"radius",
|
||||
"fillet",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def schema(cls) -> Mapping[str, Tuple[str, str]]:
|
||||
|
||||
@@ -28,7 +28,6 @@ from .base import ToolBitShape
|
||||
|
||||
class ToolBitShapeReamer(ToolBitShape):
|
||||
name = "Reamer"
|
||||
aliases = ("reamer",)
|
||||
|
||||
@classmethod
|
||||
def schema(cls) -> Mapping[str, Tuple[str, str]]:
|
||||
|
||||
@@ -28,7 +28,6 @@ from .base import ToolBitShape
|
||||
|
||||
class ToolBitShapeSlittingSaw(ToolBitShape):
|
||||
name = "SlittingSaw"
|
||||
aliases = "slittingsaw", "slitting-saw"
|
||||
|
||||
@classmethod
|
||||
def schema(cls) -> Mapping[str, Tuple[str, str]]:
|
||||
|
||||
@@ -28,7 +28,6 @@ from .base import ToolBitShape
|
||||
|
||||
class ToolBitShapeTap(ToolBitShape):
|
||||
name = "Tap"
|
||||
aliases = ("Tap",)
|
||||
|
||||
@classmethod
|
||||
def schema(cls) -> Mapping[str, Tuple[str, str]]:
|
||||
|
||||
@@ -26,7 +26,6 @@ from .base import ToolBitShape
|
||||
|
||||
class ToolBitShapeTaperedBallNose(ToolBitShape):
|
||||
name: str = "TaperedBallNose"
|
||||
aliases = ("taperedballnose",)
|
||||
|
||||
@classmethod
|
||||
def schema(cls) -> Mapping[str, Tuple[str, str]]:
|
||||
|
||||
@@ -28,7 +28,6 @@ from .base import ToolBitShape
|
||||
|
||||
class ToolBitShapeThreadMill(ToolBitShape):
|
||||
name = "ThreadMill"
|
||||
aliases = "threadmill", "thread-mill"
|
||||
|
||||
@classmethod
|
||||
def schema(cls) -> Mapping[str, Tuple[str, str]]:
|
||||
|
||||
@@ -28,7 +28,6 @@ from .base import ToolBitShape
|
||||
|
||||
class ToolBitShapeVBit(ToolBitShape):
|
||||
name = "VBit"
|
||||
aliases = "vbit", "v-bit"
|
||||
|
||||
@classmethod
|
||||
def schema(cls) -> Mapping[str, Tuple[str, str]]:
|
||||
|
||||
@@ -31,7 +31,8 @@ class CuttingToolMixin:
|
||||
It is used to determine if the tool bit can be used for chip removal.
|
||||
"""
|
||||
|
||||
def __init__(self, obj, *args, **kwargs):
|
||||
def _init_cutting_properties(self, obj):
|
||||
"""Initialize cutting tool properties. Must be called explicitly after obj is created."""
|
||||
obj.addProperty(
|
||||
"App::PropertyLength",
|
||||
"Chipload",
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
# ***************************************************************************
|
||||
import FreeCAD
|
||||
import Path
|
||||
from typing import Optional, Mapping
|
||||
from ...shape import ToolBitShapeBallend
|
||||
from ..mixins import RotaryToolBitMixin, CuttingToolMixin
|
||||
from .base import ToolBit
|
||||
@@ -30,10 +31,12 @@ from .base import ToolBit
|
||||
class ToolBitBallend(ToolBit, CuttingToolMixin, RotaryToolBitMixin):
|
||||
SHAPE_CLASS = ToolBitShapeBallend
|
||||
|
||||
def __init__(self, shape: ToolBitShapeBallend, id: str | None = None):
|
||||
def __init__(
|
||||
self, shape: ToolBitShapeBallend, id: str | None = None, attrs: Optional[Mapping] = None
|
||||
):
|
||||
Path.Log.track(f"ToolBitBallend __init__ called with shape: {shape}, id: {id}")
|
||||
super().__init__(shape, id=id)
|
||||
CuttingToolMixin.__init__(self, self.obj)
|
||||
super().__init__(shape, id=id, attrs=attrs)
|
||||
self._init_cutting_properties(self.obj)
|
||||
|
||||
@property
|
||||
def summary(self) -> str:
|
||||
|
||||
@@ -83,7 +83,13 @@ class ToolBit(Asset, ABC):
|
||||
asset_type: str = "toolbit"
|
||||
SHAPE_CLASS: Type[ToolBitShape] # Abstract class attribute
|
||||
|
||||
def __init__(self, tool_bit_shape: ToolBitShape, id: Optional[str] = None):
|
||||
def __init__(
|
||||
self,
|
||||
tool_bit_shape: ToolBitShape,
|
||||
id: Optional[str] = None,
|
||||
attrs: Optional[Mapping] = None,
|
||||
):
|
||||
super().__init__()
|
||||
Path.Log.track("ToolBit __init__ called")
|
||||
self.id = id if id is not None else str(uuid.uuid4())
|
||||
self.obj = DetachedDocumentObject()
|
||||
@@ -96,10 +102,14 @@ class ToolBit(Asset, ABC):
|
||||
self.obj.ShapeID = tool_bit_shape.get_id()
|
||||
self.obj.ShapeType = tool_bit_shape.name
|
||||
self.obj.Label = tool_bit_shape.label or f"New {tool_bit_shape.name}"
|
||||
|
||||
# Initialize properties
|
||||
self._update_tool_properties()
|
||||
|
||||
# Preserve the original shape-type from attrs (e.g., "compression", "roughing")
|
||||
# This is what the user selected and should be saved back to disk
|
||||
# If not provided, default to the class name (e.g., "Endmill")
|
||||
self._shape_type = attrs.get("shape-type") if attrs else tool_bit_shape.name
|
||||
|
||||
def __eq__(self, other):
|
||||
"""Compare ToolBit objects based on their unique ID."""
|
||||
if not isinstance(other, ToolBit):
|
||||
@@ -133,6 +143,14 @@ class ToolBit(Asset, ABC):
|
||||
attrs["shape-type"] = attrs["shape"]
|
||||
shape_type = attrs.get("shape-type")
|
||||
shape_class = ToolBitShape.get_shape_class_from_id(shape_id, shape_type)
|
||||
|
||||
if shape_class and shape_type:
|
||||
shape_type_lower = shape_type.lower()
|
||||
# Normalize aliases to canonical name, but preserve subtypes
|
||||
if shape_type_lower in shape_class.aliases:
|
||||
attrs["shape-type"] = shape_class.name
|
||||
# If it's a subtype, keep it as-is (already set in attrs)
|
||||
|
||||
if not shape_class:
|
||||
Path.Log.debug(
|
||||
f"Failed to find usable shape for ID '{shape_id}'"
|
||||
@@ -149,7 +167,8 @@ class ToolBit(Asset, ABC):
|
||||
Path.Log.debug(f"ToolBit.from_dict: Shape asset {shape_asset_uri} not found.")
|
||||
# Rely on the fallback below
|
||||
else:
|
||||
return cls.from_shape(tool_bit_shape, attrs, id=attrs.get("id"))
|
||||
toolbit = cls.from_shape(tool_bit_shape, attrs, id=attrs.get("id"))
|
||||
return toolbit
|
||||
|
||||
# Ending up here means we either could not load the shape asset,
|
||||
# or we are in shallow mode and do not want to load it.
|
||||
@@ -162,7 +181,9 @@ class ToolBit(Asset, ABC):
|
||||
)
|
||||
|
||||
# Now that we have a shape, create the toolbit instance.
|
||||
return cls.from_shape(tool_bit_shape, attrs, id=attrs.get("id"))
|
||||
toolbit = cls.from_shape(tool_bit_shape, attrs, id=attrs.get("id"))
|
||||
|
||||
return toolbit
|
||||
|
||||
@classmethod
|
||||
def from_shape(
|
||||
@@ -172,7 +193,7 @@ class ToolBit(Asset, ABC):
|
||||
id: Optional[str] = None,
|
||||
) -> "ToolBit":
|
||||
selected_toolbit_subclass = cls._find_subclass_for_shape(tool_bit_shape)
|
||||
toolbit = selected_toolbit_subclass(tool_bit_shape, id=id)
|
||||
toolbit = selected_toolbit_subclass(tool_bit_shape, id=id, attrs=attrs)
|
||||
toolbit.label = attrs.get("name") or tool_bit_shape.label
|
||||
|
||||
# Get params and attributes.
|
||||
@@ -1016,3 +1037,10 @@ class ToolBit(Asset, ABC):
|
||||
This mostly exists as a safe-hold for probes, which should never rotate.
|
||||
"""
|
||||
return True
|
||||
|
||||
def get_subtype(self) -> Optional[str]:
|
||||
"""Returns the alias/subtype used to instantiate this toolbit, if any."""
|
||||
# Only return the subtype if it differs from the class name
|
||||
if self._shape_type.lower() != self._tool_bit_shape.name.lower():
|
||||
return self._shape_type.lower()
|
||||
return None
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
# ***************************************************************************
|
||||
import FreeCAD
|
||||
import Path
|
||||
from typing import Optional, Mapping
|
||||
from ...shape import ToolBitShapeBullnose
|
||||
from ..mixins import RotaryToolBitMixin, CuttingToolMixin
|
||||
from .base import ToolBit
|
||||
@@ -30,10 +31,15 @@ from .base import ToolBit
|
||||
class ToolBitBullnose(ToolBit, CuttingToolMixin, RotaryToolBitMixin):
|
||||
SHAPE_CLASS = ToolBitShapeBullnose
|
||||
|
||||
def __init__(self, tool_bit_shape: ToolBitShapeBullnose, id: str | None = None):
|
||||
def __init__(
|
||||
self,
|
||||
tool_bit_shape: ToolBitShapeBullnose,
|
||||
id: str | None = None,
|
||||
attrs: Optional[Mapping] = None,
|
||||
):
|
||||
Path.Log.track(f"ToolBitBullnose __init__ called with id: {id}")
|
||||
super().__init__(tool_bit_shape, id=id)
|
||||
CuttingToolMixin.__init__(self, self.obj)
|
||||
super().__init__(tool_bit_shape, id=id, attrs=attrs)
|
||||
self._init_cutting_properties(self.obj)
|
||||
|
||||
@property
|
||||
def summary(self) -> str:
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
# ***************************************************************************
|
||||
import FreeCAD
|
||||
import Path
|
||||
from typing import Optional, Mapping
|
||||
from ...shape import ToolBitShapeChamfer
|
||||
from ..mixins import RotaryToolBitMixin, CuttingToolMixin
|
||||
from .base import ToolBit
|
||||
@@ -30,10 +31,12 @@ from .base import ToolBit
|
||||
class ToolBitChamfer(ToolBit, CuttingToolMixin, RotaryToolBitMixin):
|
||||
SHAPE_CLASS = ToolBitShapeChamfer
|
||||
|
||||
def __init__(self, shape: ToolBitShapeChamfer, id: str | None = None):
|
||||
def __init__(
|
||||
self, shape: ToolBitShapeChamfer, id: str | None = None, attrs: Optional[Mapping] = None
|
||||
):
|
||||
Path.Log.track(f"ToolBitChamfer __init__ called with shape: {shape}, id: {id}")
|
||||
super().__init__(shape, id=id)
|
||||
CuttingToolMixin.__init__(self, self.obj)
|
||||
super().__init__(shape, id=id, attrs=attrs)
|
||||
self._init_cutting_properties(self.obj)
|
||||
|
||||
@property
|
||||
def summary(self) -> str:
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
# ***************************************************************************
|
||||
import FreeCAD
|
||||
import Path
|
||||
from typing import Optional, Mapping
|
||||
from ...shape import ToolBitShapeCustom
|
||||
from .base import ToolBit
|
||||
|
||||
@@ -29,9 +30,11 @@ from .base import ToolBit
|
||||
class ToolBitCustom(ToolBit):
|
||||
SHAPE_CLASS = ToolBitShapeCustom
|
||||
|
||||
def __init__(self, shape: ToolBitShapeCustom, id: str | None = None):
|
||||
def __init__(
|
||||
self, shape: ToolBitShapeCustom, id: str | None = None, attrs: Optional[Mapping] = None
|
||||
):
|
||||
Path.Log.track(f"ToolBitCustom __init__ called with shape: {shape}, id: {id}")
|
||||
super().__init__(shape, id=id)
|
||||
super().__init__(shape, id=id, attrs=attrs)
|
||||
|
||||
@property
|
||||
def summary(self) -> str:
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
# ***************************************************************************
|
||||
import FreeCAD
|
||||
import Path
|
||||
from typing import Optional, Mapping
|
||||
from ...shape import ToolBitShapeDovetail
|
||||
from ..mixins import RotaryToolBitMixin, CuttingToolMixin
|
||||
from .base import ToolBit
|
||||
@@ -30,10 +31,12 @@ from .base import ToolBit
|
||||
class ToolBitDovetail(ToolBit, CuttingToolMixin, RotaryToolBitMixin):
|
||||
SHAPE_CLASS = ToolBitShapeDovetail
|
||||
|
||||
def __init__(self, shape: ToolBitShapeDovetail, id: str | None = None):
|
||||
def __init__(
|
||||
self, shape: ToolBitShapeDovetail, id: str | None = None, attrs: Optional[Mapping] = None
|
||||
):
|
||||
Path.Log.track(f"ToolBitDovetail __init__ called with shape: {shape}, id: {id}")
|
||||
super().__init__(shape, id=id)
|
||||
CuttingToolMixin.__init__(self, self.obj)
|
||||
super().__init__(shape, id=id, attrs=attrs)
|
||||
self._init_cutting_properties(self.obj)
|
||||
|
||||
@property
|
||||
def summary(self) -> str:
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
# ***************************************************************************
|
||||
import FreeCAD
|
||||
import Path
|
||||
from typing import Optional, Mapping
|
||||
from ...shape import ToolBitShapeDrill
|
||||
from ..mixins import RotaryToolBitMixin, CuttingToolMixin
|
||||
from .base import ToolBit
|
||||
@@ -30,10 +31,12 @@ from .base import ToolBit
|
||||
class ToolBitDrill(ToolBit, CuttingToolMixin, RotaryToolBitMixin):
|
||||
SHAPE_CLASS = ToolBitShapeDrill
|
||||
|
||||
def __init__(self, shape: ToolBitShapeDrill, id: str | None = None):
|
||||
def __init__(
|
||||
self, shape: ToolBitShapeDrill, id: str | None = None, attrs: Optional[Mapping] = None
|
||||
):
|
||||
Path.Log.track(f"ToolBitDrill __init__ called with shape: {shape}, id: {id}")
|
||||
super().__init__(shape, id=id)
|
||||
CuttingToolMixin.__init__(self, self.obj)
|
||||
super().__init__(shape, id=id, attrs=attrs)
|
||||
self._init_cutting_properties(self.obj)
|
||||
|
||||
@property
|
||||
def summary(self) -> str:
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
# ***************************************************************************
|
||||
import FreeCAD
|
||||
import Path
|
||||
from typing import Optional, Mapping
|
||||
from ...shape import ToolBitShapeEndmill
|
||||
from ..mixins import RotaryToolBitMixin, CuttingToolMixin
|
||||
from .base import ToolBit
|
||||
@@ -30,10 +31,12 @@ from .base import ToolBit
|
||||
class ToolBitEndmill(ToolBit, CuttingToolMixin, RotaryToolBitMixin):
|
||||
SHAPE_CLASS = ToolBitShapeEndmill
|
||||
|
||||
def __init__(self, shape: ToolBitShapeEndmill, id: str | None = None):
|
||||
def __init__(
|
||||
self, shape: ToolBitShapeEndmill, id: str | None = None, attrs: Optional[Mapping] = None
|
||||
):
|
||||
Path.Log.track(f"ToolBitEndmill __init__ called with shape: {shape}, id: {id}")
|
||||
super().__init__(shape, id=id)
|
||||
CuttingToolMixin.__init__(self, self.obj)
|
||||
super().__init__(shape, id=id, attrs=attrs)
|
||||
self._init_cutting_properties(self.obj)
|
||||
|
||||
@property
|
||||
def summary(self) -> str:
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
# ***************************************************************************
|
||||
import FreeCAD
|
||||
import Path
|
||||
from typing import Optional, Mapping
|
||||
from ...shape import ToolBitShapeProbe
|
||||
from .base import ToolBit
|
||||
|
||||
@@ -29,9 +30,11 @@ from .base import ToolBit
|
||||
class ToolBitProbe(ToolBit):
|
||||
SHAPE_CLASS = ToolBitShapeProbe
|
||||
|
||||
def __init__(self, shape: ToolBitShapeProbe, id: str | None = None):
|
||||
def __init__(
|
||||
self, shape: ToolBitShapeProbe, id: str | None = None, attrs: Optional[Mapping] = None
|
||||
):
|
||||
Path.Log.track(f"ToolBitProbe __init__ called with shape: {shape}, id: {id}")
|
||||
super().__init__(shape, id=id)
|
||||
super().__init__(shape, id=id, attrs=attrs)
|
||||
self.obj.SpindleDirection = "None"
|
||||
self.obj.setEditorMode("SpindleDirection", 2) # Read-only
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
# ***************************************************************************
|
||||
import FreeCAD
|
||||
import Path
|
||||
from typing import Optional, Mapping
|
||||
from ...shape import ToolBitShapeRadius
|
||||
from ..mixins import RotaryToolBitMixin, CuttingToolMixin
|
||||
from .base import ToolBit
|
||||
@@ -30,10 +31,12 @@ from .base import ToolBit
|
||||
class ToolBitRadius(ToolBit, CuttingToolMixin, RotaryToolBitMixin):
|
||||
SHAPE_CLASS = ToolBitShapeRadius
|
||||
|
||||
def __init__(self, shape: ToolBitShapeRadius, id: str | None = None):
|
||||
def __init__(
|
||||
self, shape: ToolBitShapeRadius, id: str | None = None, attrs: Optional[Mapping] = None
|
||||
):
|
||||
Path.Log.track(f"ToolBitRadius __init__ called with shape: {shape}, id: {id}")
|
||||
super().__init__(shape, id=id)
|
||||
CuttingToolMixin.__init__(self, self.obj)
|
||||
super().__init__(shape, id=id, attrs=attrs)
|
||||
self._init_cutting_properties(self.obj)
|
||||
|
||||
@property
|
||||
def summary(self) -> str:
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
# ***************************************************************************
|
||||
import FreeCAD
|
||||
import Path
|
||||
from typing import Optional, Mapping
|
||||
from ...shape import ToolBitShapeReamer
|
||||
from ..mixins import RotaryToolBitMixin, CuttingToolMixin
|
||||
from .base import ToolBit
|
||||
@@ -30,10 +31,12 @@ from .base import ToolBit
|
||||
class ToolBitReamer(ToolBit, CuttingToolMixin, RotaryToolBitMixin):
|
||||
SHAPE_CLASS = ToolBitShapeReamer
|
||||
|
||||
def __init__(self, shape: ToolBitShapeReamer, id: str | None = None):
|
||||
def __init__(
|
||||
self, shape: ToolBitShapeReamer, id: str | None = None, attrs: Optional[Mapping] = None
|
||||
):
|
||||
Path.Log.track(f"ToolBitReamer __init__ called with shape: {shape}, id: {id}")
|
||||
super().__init__(shape, id=id)
|
||||
CuttingToolMixin.__init__(self, self.obj)
|
||||
super().__init__(shape, id=id, attrs=attrs)
|
||||
self._init_cutting_properties(self.obj)
|
||||
|
||||
@property
|
||||
def summary(self) -> str:
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
# ***************************************************************************
|
||||
import FreeCAD
|
||||
import Path
|
||||
from typing import Optional, Mapping
|
||||
from ...shape import ToolBitShapeSlittingSaw
|
||||
from ..mixins import RotaryToolBitMixin, CuttingToolMixin
|
||||
from .base import ToolBit
|
||||
@@ -30,10 +31,12 @@ from .base import ToolBit
|
||||
class ToolBitSlittingSaw(ToolBit, CuttingToolMixin, RotaryToolBitMixin):
|
||||
SHAPE_CLASS = ToolBitShapeSlittingSaw
|
||||
|
||||
def __init__(self, shape: ToolBitShapeSlittingSaw, id: str | None = None):
|
||||
def __init__(
|
||||
self, shape: ToolBitShapeSlittingSaw, id: str | None = None, attrs: Optional[Mapping] = None
|
||||
):
|
||||
Path.Log.track(f"ToolBitSlittingSaw __init__ called with shape: {shape}, id: {id}")
|
||||
super().__init__(shape, id=id)
|
||||
CuttingToolMixin.__init__(self, self.obj)
|
||||
super().__init__(shape, id=id, attrs=attrs)
|
||||
self._init_cutting_properties(self.obj)
|
||||
|
||||
@property
|
||||
def summary(self) -> str:
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
# ***************************************************************************
|
||||
import FreeCAD
|
||||
import Path
|
||||
from typing import Optional, Mapping
|
||||
from ...shape import ToolBitShapeTap
|
||||
from ..mixins import RotaryToolBitMixin, CuttingToolMixin
|
||||
from .base import ToolBit
|
||||
@@ -31,10 +32,12 @@ from ..util import is_imperial_pitch
|
||||
class ToolBitTap(ToolBit, CuttingToolMixin, RotaryToolBitMixin):
|
||||
SHAPE_CLASS = ToolBitShapeTap
|
||||
|
||||
def __init__(self, shape: ToolBitShapeTap, id: str | None = None):
|
||||
def __init__(
|
||||
self, shape: ToolBitShapeTap, id: str | None = None, attrs: Optional[Mapping] = None
|
||||
):
|
||||
Path.Log.track(f"ToolBitTap __init__ called with shape: {shape}, id: {id}")
|
||||
super().__init__(shape, id=id)
|
||||
CuttingToolMixin.__init__(self, self.obj)
|
||||
super().__init__(shape, id=id, attrs=attrs)
|
||||
self._init_cutting_properties(self.obj)
|
||||
|
||||
@property
|
||||
def summary(self) -> str:
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
################################################################################
|
||||
import FreeCAD
|
||||
import Path
|
||||
from typing import Optional, Mapping
|
||||
from ...shape import ToolBitShapeTaperedBallNose
|
||||
from ..mixins import RotaryToolBitMixin, CuttingToolMixin
|
||||
from .base import ToolBit
|
||||
@@ -28,10 +29,15 @@ from .base import ToolBit
|
||||
class ToolBitTaperedBallNose(ToolBit, CuttingToolMixin, RotaryToolBitMixin):
|
||||
SHAPE_CLASS = ToolBitShapeTaperedBallNose
|
||||
|
||||
def __init__(self, shape: ToolBitShapeTaperedBallNose, id: str | None = None):
|
||||
Path.Log.track(f"ToolBitTaperedBallNose __init__ called with shape: {shape}, id: {id}")
|
||||
super().__init__(shape, id=id)
|
||||
CuttingToolMixin.__init__(self, self.obj)
|
||||
def __init__(
|
||||
self,
|
||||
tool_bit_shape: ToolBitShapeTaperedBallNose,
|
||||
id: str | None = None,
|
||||
attrs: Optional[Mapping] = None,
|
||||
):
|
||||
Path.Log.track(f"ToolBitTaperedBallNose __init__ called with id: {id}")
|
||||
super().__init__(tool_bit_shape, id=id, attrs=attrs)
|
||||
self._init_cutting_properties(self.obj)
|
||||
|
||||
@property
|
||||
def summary(self) -> str:
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
# ***************************************************************************
|
||||
import FreeCAD
|
||||
import Path
|
||||
from typing import Optional, Mapping
|
||||
from ...shape import ToolBitShapeThreadMill
|
||||
from ..mixins import RotaryToolBitMixin, CuttingToolMixin
|
||||
from .base import ToolBit
|
||||
@@ -30,10 +31,12 @@ from .base import ToolBit
|
||||
class ToolBitThreadMill(ToolBit, CuttingToolMixin, RotaryToolBitMixin):
|
||||
SHAPE_CLASS = ToolBitShapeThreadMill
|
||||
|
||||
def __init__(self, shape: ToolBitShapeThreadMill, id: str | None = None):
|
||||
Path.Log.track(f"ToolBitThreadMill __init__ called with shape: {shape}, id: {id}")
|
||||
super().__init__(shape, id=id)
|
||||
CuttingToolMixin.__init__(self, self.obj)
|
||||
def __init__(
|
||||
self, shape: ToolBitShapeThreadMill, id: str | None = None, attrs: Optional[Mapping] = None
|
||||
):
|
||||
Path.Log.track(f"ToolBitThreadmill __init__ called with shape: {shape}, id: {id}")
|
||||
super().__init__(shape, id=id, attrs=attrs)
|
||||
self._init_cutting_properties(self.obj)
|
||||
|
||||
@property
|
||||
def summary(self) -> str:
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
# ***************************************************************************
|
||||
import FreeCAD
|
||||
import Path
|
||||
from typing import Optional, Mapping
|
||||
from ...shape import ToolBitShapeVBit
|
||||
from ..mixins import RotaryToolBitMixin, CuttingToolMixin
|
||||
from .base import ToolBit
|
||||
@@ -30,10 +31,12 @@ from .base import ToolBit
|
||||
class ToolBitVBit(ToolBit, CuttingToolMixin, RotaryToolBitMixin):
|
||||
SHAPE_CLASS = ToolBitShapeVBit
|
||||
|
||||
def __init__(self, shape: ToolBitShapeVBit, id: str | None = None):
|
||||
def __init__(
|
||||
self, shape: ToolBitShapeVBit, id: str | None = None, attrs: Optional[Mapping] = None
|
||||
):
|
||||
Path.Log.track(f"ToolBitVBit __init__ called with shape: {shape}, id: {id}")
|
||||
super().__init__(shape, id=id)
|
||||
CuttingToolMixin.__init__(self, self.obj)
|
||||
super().__init__(shape, id=id, attrs=attrs)
|
||||
self._init_cutting_properties(self.obj)
|
||||
|
||||
@property
|
||||
def summary(self) -> str:
|
||||
|
||||
@@ -81,6 +81,7 @@ class ToolBitBrowserWidget(QtGui.QWidget):
|
||||
# UI Elements
|
||||
self._search_edit = QtGui.QLineEdit()
|
||||
self._search_edit.setPlaceholderText("Search toolbits...")
|
||||
self._search_edit.setClearButtonEnabled(True)
|
||||
|
||||
# Sorting dropdown
|
||||
self._sort_combo = QtGui.QComboBox()
|
||||
|
||||
@@ -27,6 +27,8 @@ from typing import Optional
|
||||
from PySide import QtGui, QtCore
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
import os
|
||||
from Path.Preferences import getAssetPath
|
||||
from ...shape.ui.shapewidget import ShapeWidget
|
||||
from ...docobject.ui import DocumentObjectEditorWidget
|
||||
from ..models.base import ToolBit
|
||||
@@ -59,6 +61,14 @@ class ToolBitPropertiesWidget(QtGui.QWidget):
|
||||
|
||||
# UI Elements
|
||||
self._label_edit = QtGui.QLineEdit()
|
||||
self._toolbit_type_container = QtGui.QWidget()
|
||||
container_layout = QtGui.QHBoxLayout(self._toolbit_type_container)
|
||||
container_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self._toolbit_type_edit = QtGui.QLineEdit()
|
||||
self._toolbit_type_edit.setReadOnly(True)
|
||||
self._toolbit_type_combo = QtGui.QComboBox()
|
||||
container_layout.addWidget(self._toolbit_type_edit)
|
||||
container_layout.addWidget(self._toolbit_type_combo)
|
||||
self._id_label = QtGui.QLabel() # Read-only ID
|
||||
self._id_label.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)
|
||||
|
||||
@@ -74,7 +84,7 @@ class ToolBitPropertiesWidget(QtGui.QWidget):
|
||||
toolbit_group_box = QtGui.QGroupBox(translate("CAM", "Toolbit"))
|
||||
form_layout = QtGui.QFormLayout(toolbit_group_box)
|
||||
form_layout.addRow(translate("CAM", "Label:"), self._label_edit)
|
||||
# form_layout.addRow(translate("CAM", "ID:"), self._id_label)
|
||||
form_layout.addRow(translate("CAM", "Toolbit Type:"), self._toolbit_type_container)
|
||||
|
||||
# Optional tool number edit field.
|
||||
self._tool_no_edit = QtGui.QSpinBox()
|
||||
@@ -112,6 +122,7 @@ class ToolBitPropertiesWidget(QtGui.QWidget):
|
||||
# Connections
|
||||
self._label_edit.editingFinished.connect(self._on_label_changed)
|
||||
self._tool_no_edit.valueChanged.connect(self._on_tool_no_changed)
|
||||
self._toolbit_type_combo.currentIndexChanged.connect(self._on_toolbit_type_changed)
|
||||
self._property_editor.propertyChanged.connect(self.toolBitChanged)
|
||||
|
||||
if toolbit:
|
||||
@@ -131,6 +142,14 @@ class ToolBitPropertiesWidget(QtGui.QWidget):
|
||||
self._tool_no = value
|
||||
self.toolNoChanged.emit(value)
|
||||
|
||||
def _on_toolbit_type_changed(self, index):
|
||||
"""Update the toolbit's type when the combo changes."""
|
||||
if self._toolbit:
|
||||
selected = self._toolbit_type_combo.itemData(index)
|
||||
self._toolbit._shape_type = selected # Save the user's selection
|
||||
self._toolbit.obj.ShapeType = selected
|
||||
self.toolBitChanged.emit()
|
||||
|
||||
def load_toolbit(self, toolbit: ToolBit):
|
||||
"""Load a ToolBit object into the editor."""
|
||||
# Set schema based on the toolbit's Units property if available
|
||||
@@ -150,6 +169,10 @@ class ToolBitPropertiesWidget(QtGui.QWidget):
|
||||
# Clear or disable fields if toolbit is invalid
|
||||
self._label_edit.clear()
|
||||
self._label_edit.setEnabled(False)
|
||||
self._toolbit_type_edit.clear()
|
||||
self._toolbit_type_edit.hide()
|
||||
self._toolbit_type_combo.clear()
|
||||
self._toolbit_type_combo.hide()
|
||||
self._id_label.clear()
|
||||
self._tool_no_edit.clear()
|
||||
self._property_editor.setObject(None)
|
||||
@@ -166,6 +189,63 @@ class ToolBitPropertiesWidget(QtGui.QWidget):
|
||||
self._label_edit.setEnabled(True)
|
||||
self._label_edit.setText(self._toolbit.obj.Label)
|
||||
self._id_label.setText(self._toolbit.get_id())
|
||||
|
||||
# Set toolbit type
|
||||
shape_class = self._toolbit._tool_bit_shape.__class__
|
||||
base = shape_class.name
|
||||
subtypes = shape_class.subtypes
|
||||
current_type = (
|
||||
self._toolbit._shape_type
|
||||
if hasattr(self._toolbit, "_shape_type")
|
||||
else shape_class.name.lower()
|
||||
)
|
||||
shape_dir = os.path.join(getAssetPath(), "Tools", "Shape")
|
||||
|
||||
has_file = (
|
||||
bool(current_type)
|
||||
and current_type != base.lower()
|
||||
and os.path.isfile(os.path.join(shape_dir, f"{current_type}.fcstd"))
|
||||
)
|
||||
|
||||
editable_types = [base.lower()]
|
||||
for subtype in subtypes:
|
||||
if not os.path.isfile(os.path.join(shape_dir, f"{subtype}.fcstd")):
|
||||
editable_types.append(subtype)
|
||||
|
||||
is_readonly = has_file or len(editable_types) <= 1
|
||||
|
||||
if is_readonly:
|
||||
# Read-only
|
||||
raw_text = current_type or base.lower()
|
||||
display_text = raw_text.replace("_", " ").replace("-", " ").title()
|
||||
self._toolbit_type_edit.setText(display_text)
|
||||
self._toolbit_type_edit.show()
|
||||
self._toolbit_type_combo.hide()
|
||||
else:
|
||||
# Editable - populate combo with aliases that don't have independent shape files
|
||||
self._toolbit_type_combo.clear()
|
||||
for type_name in editable_types:
|
||||
display = type_name.replace("_", " ").replace("-", " ").title()
|
||||
self._toolbit_type_combo.addItem(display, type_name)
|
||||
|
||||
for i in range(self._toolbit_type_combo.count()):
|
||||
if self._toolbit_type_combo.itemData(i) == current_type:
|
||||
self._toolbit_type_combo.setCurrentIndex(i)
|
||||
break
|
||||
else:
|
||||
# No match found - check if current_type is the base class name
|
||||
if self._toolbit_type_combo.count() > 0:
|
||||
if current_type == base or current_type == base.lower():
|
||||
# It's using the class name (e.g., "Endmill") - keep it
|
||||
# Set to first item in combo for display but don't change _shape_type
|
||||
self._toolbit_type_combo.setCurrentIndex(0)
|
||||
else:
|
||||
# Unknown type - default to first item (class name)
|
||||
self._toolbit_type_combo.setCurrentIndex(0)
|
||||
self._toolbit._shape_type = editable_types[0] if editable_types else base
|
||||
self._toolbit_type_edit.hide()
|
||||
self._toolbit_type_combo.show()
|
||||
|
||||
self._tool_no_edit.setValue(int(self._tool_no or 1))
|
||||
|
||||
# Get properties and suffixes
|
||||
|
||||
@@ -23,32 +23,42 @@
|
||||
|
||||
"""ToolBit selector dialog."""
|
||||
|
||||
from PySide import QtWidgets
|
||||
from PySide import QtWidgets, QtGui
|
||||
import FreeCAD
|
||||
from ...camassets import cam_assets
|
||||
from ...toolbit import ToolBit
|
||||
from .browser import ToolBitBrowserWidget
|
||||
from ...library.ui.browser import LibraryBrowserWithCombo
|
||||
|
||||
|
||||
class ToolBitSelector(QtWidgets.QDialog):
|
||||
"""
|
||||
A dialog for selecting ToolBits using the ToolBitBrowserWidget.
|
||||
A dialog for selecting ToolBits using the LibraryBrowserWithCombo.
|
||||
Can show tools from libraries or all available toolbits.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, parent=None, compact=False, button_label=FreeCAD.Qt.translate("CAM", "Add Tool")
|
||||
self,
|
||||
parent=None,
|
||||
compact=False,
|
||||
button_label=FreeCAD.Qt.translate("CAM", "Add Tool"),
|
||||
show_all_tools=False,
|
||||
):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setMinimumSize(600, 400)
|
||||
|
||||
self.setMinimumSize(700, 500)
|
||||
self.setWindowTitle(FreeCAD.Qt.translate("CAM", "Select Toolbit"))
|
||||
|
||||
self._browser_widget = ToolBitBrowserWidget(cam_assets, compact=compact)
|
||||
# Use LibraryBrowserWithCombo which handles library selection and "All Tools" option
|
||||
self._browser_widget = LibraryBrowserWithCombo(
|
||||
asset_manager=cam_assets,
|
||||
store="local",
|
||||
compact=compact,
|
||||
show_all_tools=show_all_tools,
|
||||
)
|
||||
|
||||
# Create OK and Cancel buttons
|
||||
self._ok_button = QtWidgets.QPushButton(button_label)
|
||||
self._cancel_button = QtWidgets.QPushButton("Cancel")
|
||||
self._cancel_button = QtWidgets.QPushButton(FreeCAD.Qt.translate("CAM", "Cancel"))
|
||||
|
||||
# Connect buttons to their actions
|
||||
self._ok_button.clicked.connect(self.accept)
|
||||
@@ -70,21 +80,47 @@ class ToolBitSelector(QtWidgets.QDialog):
|
||||
self._browser_widget.toolSelected.connect(self._on_tool_selected)
|
||||
self._browser_widget.itemDoubleClicked.connect(self.accept)
|
||||
|
||||
self._selected_tool_uri = None
|
||||
# Initialize the browser
|
||||
self._browser_widget.refresh()
|
||||
|
||||
def _on_tool_selected(self, uri):
|
||||
"""Enables/disables OK button based on selection."""
|
||||
self._selected_tool_uri = uri
|
||||
self._ok_button.setEnabled(uri is not None)
|
||||
|
||||
def get_selected_tool_uri(self):
|
||||
"""Returns the URI of the selected tool bit."""
|
||||
return self._selected_tool_uri
|
||||
|
||||
def get_selected_tool(self) -> ToolBit:
|
||||
"""Returns the selected ToolBit object, or None if none selected."""
|
||||
uri = self.get_selected_tool_uri()
|
||||
if uri:
|
||||
# Assuming ToolBit.from_uri exists and loads the ToolBit object
|
||||
return cam_assets.get(uri)
|
||||
"""Returns the first selected ToolBit object, or None if none selected."""
|
||||
tools = self.get_selected_tools()
|
||||
return tools[0] if tools else None
|
||||
|
||||
def get_selected_tools(self) -> list:
|
||||
"""Returns a list of all selected ToolBit objects."""
|
||||
return self._browser_widget.get_selected_bits()
|
||||
|
||||
def get_tool_numbers(self) -> dict:
|
||||
"""
|
||||
Returns a dict mapping ToolBit URIs to tool numbers.
|
||||
If a library is selected, uses library tool numbers.
|
||||
If \"All Tools\" is selected, returns empty dict (auto-increment).
|
||||
"""
|
||||
tool_numbers = {}
|
||||
|
||||
# Only get library numbers if a specific library is selected
|
||||
if self._browser_widget.current_library:
|
||||
for toolbit in self.get_selected_tools():
|
||||
tool_no = self._browser_widget.get_tool_no_from_current_library(toolbit)
|
||||
if tool_no is not None:
|
||||
tool_numbers[str(toolbit.get_uri())] = tool_no
|
||||
|
||||
return tool_numbers # Empty dict means auto-increment for all
|
||||
|
||||
def get_tool_number(self):
|
||||
"""
|
||||
Returns the tool number for the first selected tool.
|
||||
Kept for backward compatibility.
|
||||
"""
|
||||
tool_numbers = self.get_tool_numbers()
|
||||
if tool_numbers:
|
||||
first_tool = self.get_selected_tool()
|
||||
if first_tool:
|
||||
return tool_numbers.get(str(first_tool.get_uri()))
|
||||
return None
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2025 Billy <[email protected]> *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
# * under the terms of the GNU Lesser General Public License as *
|
||||
# * published by the Free Software Foundation, either version 2.1 of the *
|
||||
# * License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * FreeCAD 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 *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with FreeCAD. If not, see *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import FreeCAD
|
||||
from ...docobject.ui.docobject import _get_label_text
|
||||
|
||||
|
||||
class ToolBitTypeFilterMixin:
|
||||
"""
|
||||
Mixin providing shared methods for filtering toolbits by type and subtype.
|
||||
|
||||
Classes using this mixin should have:
|
||||
- _all_assets: list of ToolBit objects
|
||||
- _tool_type_combo: QComboBox for type selection
|
||||
- _selected_tool_type: Optional[str] for storing current selection
|
||||
|
||||
Classes must implement:
|
||||
- _get_assets_for_type_filter() -> list: Returns list of assets to extract types from
|
||||
- _on_type_filter_changed(): Called when type filter changes
|
||||
"""
|
||||
|
||||
def _get_available_tool_types(self, assets):
|
||||
"""Get all available tool types and subtypes, grouped for display.
|
||||
|
||||
Args:
|
||||
assets: List of ToolBit objects to extract types from
|
||||
|
||||
Returns:
|
||||
List of tuples: (display_text, actual_value)
|
||||
"""
|
||||
# Build a mapping: {ParentType: {actual_subtype: display_subtype}}
|
||||
type_map = {}
|
||||
for asset in assets:
|
||||
parent = asset.get_shape_name() # Preserve original case (e.g., "Probe")
|
||||
subtype = asset.get_subtype()
|
||||
if subtype:
|
||||
# Preserve underscores/hyphens but make displayable
|
||||
subtype_disp = subtype.replace("_", " ").replace("-", " ").title()
|
||||
type_map.setdefault(parent, {})[subtype] = subtype_disp
|
||||
else:
|
||||
type_map.setdefault(parent, {})
|
||||
|
||||
# Flatten for combo: parent, then indented subtypes
|
||||
# Return tuples of (display, value)
|
||||
result = []
|
||||
for parent in sorted(type_map):
|
||||
parent_display = _get_label_text(parent, keep_case=True, preserve_consecutive_caps=True)
|
||||
result.append((parent_display, parent)) # Parent with formatted display
|
||||
for subtype_val, subtype_disp in sorted(type_map[parent].items()):
|
||||
subtype_display = _get_label_text(
|
||||
subtype_val, keep_case=True, preserve_consecutive_caps=True
|
||||
)
|
||||
result.append(
|
||||
(f" {subtype_display}", subtype_val)
|
||||
) # Indent display, preserve value
|
||||
return result
|
||||
|
||||
def _get_filtered_assets(self, assets):
|
||||
"""Filter assets by selected type or subtype, showing subtypes under parent.
|
||||
|
||||
Args:
|
||||
assets: List of ToolBit objects to filter
|
||||
|
||||
Returns:
|
||||
Filtered list of ToolBit objects
|
||||
"""
|
||||
if self._tool_type_combo.currentIndex() == 0:
|
||||
return assets
|
||||
|
||||
# Get the actual value (not display text) from combo item data
|
||||
sel_value = self._tool_type_combo.currentData()
|
||||
if not sel_value:
|
||||
return assets
|
||||
|
||||
# Check if it's a subtype by looking at all subtypes
|
||||
all_subtypes = set()
|
||||
for asset in assets:
|
||||
subtype = asset.get_subtype()
|
||||
if subtype:
|
||||
all_subtypes.add(subtype)
|
||||
|
||||
if sel_value in all_subtypes:
|
||||
# It's a subtype - filter by exact subtype match
|
||||
return [a for a in assets if a.get_subtype() == sel_value]
|
||||
else:
|
||||
# It's a parent type - show all with this shape name (including those without subtypes)
|
||||
return [a for a in assets if a.get_shape_name() == sel_value]
|
||||
|
||||
def _update_tool_type_combo(self):
|
||||
"""Update the tool type combo box with available types.
|
||||
|
||||
Calls _get_assets_for_type_filter() to get the list of assets.
|
||||
"""
|
||||
current_data = self._tool_type_combo.currentData() # Save current selection by data
|
||||
self._tool_type_combo.blockSignals(True)
|
||||
try:
|
||||
self._tool_type_combo.clear()
|
||||
self._tool_type_combo.addItem(FreeCAD.Qt.translate("CAM", "All Toolbit Types"), None)
|
||||
|
||||
assets = self._get_assets_for_type_filter()
|
||||
available_types = self._get_available_tool_types(assets)
|
||||
for display_text, value in available_types:
|
||||
self._tool_type_combo.addItem(display_text, value)
|
||||
|
||||
# Restore previous selection if it still exists
|
||||
if current_data is not None:
|
||||
for i in range(self._tool_type_combo.count()):
|
||||
if self._tool_type_combo.itemData(i) == current_data:
|
||||
self._tool_type_combo.setCurrentIndex(i)
|
||||
break
|
||||
else:
|
||||
self._tool_type_combo.setCurrentIndex(0)
|
||||
else:
|
||||
self._tool_type_combo.setCurrentIndex(0)
|
||||
finally:
|
||||
self._tool_type_combo.blockSignals(False)
|
||||
|
||||
def _on_tool_type_combo_changed(self, index):
|
||||
"""Handle tool type filter selection change.
|
||||
|
||||
Stores the current selection and calls _on_type_filter_changed()
|
||||
which should be implemented by the subclass.
|
||||
"""
|
||||
# Store both text (for backward compat) and data
|
||||
self._selected_tool_type = self._tool_type_combo.currentText()
|
||||
self._on_type_filter_changed()
|
||||
|
||||
def _get_assets_for_type_filter(self):
|
||||
"""Returns the list of assets to use for type filtering.
|
||||
|
||||
Subclasses should override this method to provide their asset list.
|
||||
Default implementation returns _all_assets if it exists.
|
||||
"""
|
||||
return getattr(self, "_all_assets", [])
|
||||
|
||||
def _on_type_filter_changed(self):
|
||||
"""Called when the type filter changes.
|
||||
|
||||
Subclasses should override this to update their UI accordingly.
|
||||
Default implementation does nothing.
|
||||
"""
|
||||
pass
|
||||
|
||||
def _apply_type_and_search_filter(self, assets, search_term=""):
|
||||
"""Apply both type and search filtering to assets.
|
||||
|
||||
Args:
|
||||
assets: List of ToolBit objects to filter
|
||||
search_term: Optional search string to filter by (searches label and summary)
|
||||
|
||||
Returns:
|
||||
Filtered list of ToolBit objects
|
||||
"""
|
||||
# First apply type filter
|
||||
filtered_assets = self._get_filtered_assets(assets)
|
||||
|
||||
# Then apply search filter if provided
|
||||
if search_term:
|
||||
search_term_lower = search_term.lower()
|
||||
search_filtered = []
|
||||
for asset in filtered_assets:
|
||||
if search_term_lower in asset.label.lower():
|
||||
search_filtered.append(asset)
|
||||
continue
|
||||
if search_term_lower in asset.summary.lower():
|
||||
search_filtered.append(asset)
|
||||
continue
|
||||
# Also search in tool type
|
||||
if hasattr(asset, "get_shape_name"):
|
||||
tool_type = asset.get_shape_name()
|
||||
if tool_type and search_term_lower in tool_type.lower():
|
||||
search_filtered.append(asset)
|
||||
filtered_assets = search_filtered
|
||||
|
||||
return filtered_assets
|
||||
|
||||
def _refresh_filtered_list(self, tool_list_widget, search_edit, assets):
|
||||
"""Refresh the list widget with filtered assets.
|
||||
|
||||
Args:
|
||||
tool_list_widget: The QListWidget to update
|
||||
search_edit: The QLineEdit containing search text
|
||||
assets: List of ToolBit objects to filter and display
|
||||
"""
|
||||
tool_list_widget.clear_list()
|
||||
search_term = search_edit.text()
|
||||
filtered_assets = self._apply_type_and_search_filter(assets, search_term)
|
||||
|
||||
for asset in filtered_assets:
|
||||
tool_list_widget.add_toolbit(asset)
|
||||
@@ -0,0 +1,107 @@
|
||||
{
|
||||
"Ballend": {
|
||||
"aliases": [
|
||||
"ballend"
|
||||
],
|
||||
"subtypes": []
|
||||
},
|
||||
"Bullnose": {
|
||||
"aliases": [
|
||||
"bullnose",
|
||||
"torus"
|
||||
],
|
||||
"subtypes": []
|
||||
},
|
||||
"Chamfer": {
|
||||
"aliases": [
|
||||
"chamfer"
|
||||
],
|
||||
"subtypes": []
|
||||
},
|
||||
"Custom": {
|
||||
"aliases": [
|
||||
"custom"
|
||||
],
|
||||
"subtypes": []
|
||||
},
|
||||
"Dovetail": {
|
||||
"aliases": [
|
||||
"dovetail"
|
||||
],
|
||||
"subtypes": []
|
||||
},
|
||||
"Drill": {
|
||||
"aliases": [
|
||||
"drill"
|
||||
],
|
||||
"subtypes": [
|
||||
"jobber",
|
||||
"stub",
|
||||
"spot",
|
||||
"center",
|
||||
"brad_point"
|
||||
]
|
||||
},
|
||||
"Endmill": {
|
||||
"aliases": [
|
||||
"endmill"
|
||||
],
|
||||
"subtypes": []
|
||||
},
|
||||
"Probe": {
|
||||
"aliases": [
|
||||
"probe"
|
||||
],
|
||||
"subtypes": []
|
||||
},
|
||||
"Radius": {
|
||||
"aliases": [
|
||||
"radius",
|
||||
"fillet"
|
||||
],
|
||||
"subtypes": []
|
||||
},
|
||||
"Reamer": {
|
||||
"aliases": [
|
||||
"reamer"
|
||||
],
|
||||
"subtypes": []
|
||||
},
|
||||
"SlittingSaw": {
|
||||
"aliases": [
|
||||
"slittingsaw",
|
||||
"slitting-saw",
|
||||
"slitting_saw"
|
||||
],
|
||||
"subtypes": []
|
||||
},
|
||||
"Tap": {
|
||||
"aliases": [
|
||||
"tap"
|
||||
],
|
||||
"subtypes": []
|
||||
},
|
||||
"TaperedBallNose": {
|
||||
"aliases": [
|
||||
"taperballnose",
|
||||
"conicalballnose"
|
||||
],
|
||||
"subtypes": []
|
||||
},
|
||||
"ThreadMill": {
|
||||
"aliases": [
|
||||
"threadmill",
|
||||
"thread-mill",
|
||||
"thread_mill"
|
||||
],
|
||||
"subtypes": []
|
||||
},
|
||||
"VBit": {
|
||||
"aliases": [
|
||||
"vbit",
|
||||
"v-bit",
|
||||
"v_bit"
|
||||
],
|
||||
"subtypes": []
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user