fixes
fix duplicate toolbits add tools to 'all tools' context menus and deletion /CamAssets/Tool/ directory structure Assets and preferences
This commit is contained in:
@@ -54,7 +54,7 @@
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout" stretch="1,0">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout" stretch="1">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetMinimumSize</enum>
|
||||
</property>
|
||||
@@ -64,18 +64,7 @@
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButtonSave">
|
||||
<property name="text">
|
||||
<string>Edit Library</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset>
|
||||
<normaloff>../resources/icons/add-library.svg</normaloff>../resources/icons/add-library.svg</iconset>
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -84,38 +73,5 @@
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>Dialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>Dialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
<connections/>
|
||||
</ui>
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>681</width>
|
||||
<height>370</height>
|
||||
<width>695</width>
|
||||
<height>308</height>
|
||||
</rect>
|
||||
</property>
|
||||
<attribute name="label">
|
||||
@@ -38,37 +38,7 @@
|
||||
<string>Defaults</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_7">
|
||||
<property name="text">
|
||||
<string>Path</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLineEdit" name="leDefaultFilePath">
|
||||
<property name="toolTip">
|
||||
<string>Path to look for templates, post processors, tool tables and other external files.
|
||||
|
||||
If left empty the macro directory is used.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="3">
|
||||
<widget class="QToolButton" name="tbDefaultFilePath">
|
||||
<property name="text">
|
||||
<string notr="true">…</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_8">
|
||||
<property name="text">
|
||||
<string>Template</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="leDefaultJobTemplate">
|
||||
<property name="toolTip">
|
||||
<string>The default template to be selected when creating a new job.
|
||||
@@ -79,7 +49,14 @@ If left empty no template will be preselected.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="3">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_8">
|
||||
<property name="text">
|
||||
<string>Template</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="3">
|
||||
<widget class="QToolButton" name="tbDefaultJobTemplate">
|
||||
<property name="text">
|
||||
<string notr="true">…</string>
|
||||
@@ -129,7 +106,7 @@ If left empty no template will be preselected.</string>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
@@ -146,8 +123,8 @@ If left empty no template will be preselected.</string>
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>681</width>
|
||||
<height>518</height>
|
||||
<width>695</width>
|
||||
<height>480</height>
|
||||
</rect>
|
||||
</property>
|
||||
<attribute name="label">
|
||||
@@ -167,7 +144,7 @@ If left empty no template will be preselected.</string>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="formLayout_2">
|
||||
<property name="fieldGrowthPolicy">
|
||||
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
|
||||
<enum>QFormLayout::FieldGrowthPolicy::AllNonFixedFieldsGrow</enum>
|
||||
</property>
|
||||
<item row="1" column="0">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
@@ -280,7 +257,7 @@ See the file save policy below on how to deal with name conflicts.</string>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<property name="fieldGrowthPolicy">
|
||||
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
|
||||
<enum>QFormLayout::FieldGrowthPolicy::AllNonFixedFieldsGrow</enum>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
@@ -345,7 +322,7 @@ See the file save policy below on how to deal with name conflicts.</string>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
@@ -362,8 +339,8 @@ See the file save policy below on how to deal with name conflicts.</string>
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>662</width>
|
||||
<height>755</height>
|
||||
<width>674</width>
|
||||
<height>619</height>
|
||||
</rect>
|
||||
</property>
|
||||
<attribute name="label">
|
||||
@@ -410,7 +387,7 @@ See the file save policy below on how to deal with name conflicts.</string>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
@@ -537,7 +514,7 @@ See the file save policy below on how to deal with name conflicts.</string>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
@@ -579,21 +556,21 @@ See the file save policy below on how to deal with name conflicts.</string>
|
||||
<item row="3" column="2">
|
||||
<widget class="QDoubleSpinBox" name="stockAxisX">
|
||||
<property name="buttonSymbols">
|
||||
<enum>QAbstractSpinBox::NoButtons</enum>
|
||||
<enum>QAbstractSpinBox::ButtonSymbols::NoButtons</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="3">
|
||||
<widget class="QDoubleSpinBox" name="stockAxisY">
|
||||
<property name="buttonSymbols">
|
||||
<enum>QAbstractSpinBox::NoButtons</enum>
|
||||
<enum>QAbstractSpinBox::ButtonSymbols::NoButtons</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="4">
|
||||
<widget class="QDoubleSpinBox" name="stockAxisZ">
|
||||
<property name="buttonSymbols">
|
||||
<enum>QAbstractSpinBox::NoButtons</enum>
|
||||
<enum>QAbstractSpinBox::ButtonSymbols::NoButtons</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -622,7 +599,7 @@ See the file save policy below on how to deal with name conflicts.</string>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
@@ -634,52 +611,12 @@ See the file save policy below on how to deal with name conflicts.</string>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="page_4">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>681</width>
|
||||
<height>171</height>
|
||||
</rect>
|
||||
</property>
|
||||
<attribute name="label">
|
||||
<string>Tools</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="toolsAbsolutePaths">
|
||||
<property name="toolTip">
|
||||
<string>References to tool bits and their shapes can either be stored with an absolute path or with a relative path to the search path.
|
||||
Generally it is recommended to use relative paths due to their flexibility and robustness to layout changes.
|
||||
Should multiple tools or tool shapes with the same name exist in different directories it can be required to use absolute paths.</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Store Absolute Paths</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_4">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_5">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
@@ -700,8 +637,6 @@ Should multiple tools or tool shapes with the same name exist in different direc
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<tabstops>
|
||||
<tabstop>leDefaultFilePath</tabstop>
|
||||
<tabstop>tbDefaultFilePath</tabstop>
|
||||
<tabstop>leDefaultJobTemplate</tabstop>
|
||||
<tabstop>tbDefaultJobTemplate</tabstop>
|
||||
<tabstop>geometryTolerance</tabstop>
|
||||
|
||||
@@ -45,11 +45,10 @@ class JobPreferencesPage:
|
||||
self.processor = {}
|
||||
|
||||
def saveSettings(self):
|
||||
filePath = self.form.leDefaultFilePath.text()
|
||||
jobTemplate = self.form.leDefaultJobTemplate.text()
|
||||
geometryTolerance = Units.Quantity(self.form.geometryTolerance.text())
|
||||
curveAccuracy = Units.Quantity(self.form.curveAccuracy.text())
|
||||
Path.Preferences.setJobDefaults(filePath, jobTemplate, geometryTolerance, curveAccuracy)
|
||||
Path.Preferences.setJobDefaults(jobTemplate, geometryTolerance, curveAccuracy)
|
||||
|
||||
if curveAccuracy:
|
||||
Path.Area.setDefaultParams(Accuracy=curveAccuracy)
|
||||
@@ -146,7 +145,6 @@ class JobPreferencesPage:
|
||||
)
|
||||
|
||||
def loadSettings(self):
|
||||
self.form.leDefaultFilePath.setText(Path.Preferences.defaultFilePath())
|
||||
self.form.leDefaultJobTemplate.setText(Path.Preferences.defaultJobTemplate())
|
||||
|
||||
blacklist = Path.Preferences.postProcessorBlacklist()
|
||||
@@ -175,7 +173,6 @@ class JobPreferencesPage:
|
||||
self.form.leOutputFile.setText(Path.Preferences.defaultOutputFile())
|
||||
self.selectComboEntry(self.form.cboOutputPolicy, Path.Preferences.defaultOutputPolicy())
|
||||
|
||||
self.form.tbDefaultFilePath.clicked.connect(self.browseDefaultFilePath)
|
||||
self.form.tbDefaultJobTemplate.clicked.connect(self.browseDefaultJobTemplate)
|
||||
self.form.postProcessorList.itemEntered.connect(self.setProcessorListTooltip)
|
||||
self.form.postProcessorList.itemChanged.connect(self.verifyAndUpdateDefaultPostProcessor)
|
||||
@@ -311,7 +308,8 @@ class JobPreferencesPage:
|
||||
self.form.defaultPostProcessorArgs.setToolTip(self.postProcessorArgsDefaultTooltip)
|
||||
|
||||
def bestGuessForFilePath(self):
|
||||
path = self.form.leDefaultFilePath.text()
|
||||
|
||||
path = Path.Preferences.defaultFilePath()
|
||||
if not path:
|
||||
path = Path.Preferences.filePath()
|
||||
return path
|
||||
@@ -326,14 +324,6 @@ class JobPreferencesPage:
|
||||
if foo:
|
||||
self.form.leDefaultJobTemplate.setText(foo)
|
||||
|
||||
def browseDefaultFilePath(self):
|
||||
path = self.bestGuessForFilePath()
|
||||
foo = QtGui.QFileDialog.getExistingDirectory(
|
||||
QtGui.QApplication.activeWindow(), "Path - External File Directory", path
|
||||
)
|
||||
if foo:
|
||||
self.form.leDefaultFilePath.setText(foo)
|
||||
|
||||
def browseOutputFile(self):
|
||||
path = self.form.leOutputFile.text()
|
||||
foo = QtGui.QFileDialog.getExistingDirectory(
|
||||
|
||||
@@ -124,22 +124,38 @@ def getDefaultAssetPath() -> Path:
|
||||
|
||||
def getAssetPath() -> pathlib.Path:
|
||||
pref = tool_preferences()
|
||||
|
||||
# Check if we have a CamAssets path already set
|
||||
cam_assets_path = pref.GetString(ToolPath, "")
|
||||
if cam_assets_path:
|
||||
return pathlib.Path(cam_assets_path)
|
||||
|
||||
# Migration: Check for legacy DefaultFilePath and use it for CamAssets
|
||||
legacy_path = defaultFilePath()
|
||||
if legacy_path:
|
||||
legacy_path_obj = pathlib.Path(legacy_path)
|
||||
if legacy_path_obj.exists() and legacy_path_obj.is_dir():
|
||||
# Migrate: Set the legacy path as the new CamAssets path
|
||||
setAssetPath(legacy_path_obj)
|
||||
return legacy_path_obj
|
||||
|
||||
# Fallback to default if no legacy path found
|
||||
default = getDefaultAssetPath()
|
||||
path = pref.GetString(ToolPath, str(default))
|
||||
return pathlib.Path(path or default)
|
||||
return pathlib.Path(default)
|
||||
|
||||
|
||||
def setAssetPath(path: pathlib.Path):
|
||||
assert path.is_dir(), f"Cannot put a non-initialized asset directory into preferences: {path}"
|
||||
if str(path) == str(getAssetPath()):
|
||||
return
|
||||
pref = tool_preferences()
|
||||
current_path = pref.GetString(ToolPath, "")
|
||||
if str(path) == current_path:
|
||||
return
|
||||
pref.SetString(ToolPath, str(path))
|
||||
_emit_change(ToolGroup, ToolPath, path)
|
||||
|
||||
|
||||
def getToolBitPath() -> pathlib.Path:
|
||||
return getAssetPath() / "Bit"
|
||||
return getAssetPath() / "Tools" / "Bit"
|
||||
|
||||
|
||||
def getLastToolLibrary() -> Optional[str]:
|
||||
@@ -212,7 +228,7 @@ def defaultFilePath():
|
||||
def filePath():
|
||||
path = defaultFilePath()
|
||||
if not path:
|
||||
path = macroFilePath()
|
||||
path = getAssetPath()
|
||||
return path
|
||||
|
||||
|
||||
@@ -248,13 +264,9 @@ def defaultJobTemplate():
|
||||
return ""
|
||||
|
||||
|
||||
def setJobDefaults(fileName, jobTemplate, geometryTolerance, curveAccuracy):
|
||||
Path.Log.track(
|
||||
"(%s='%s', %s, %s, %s)"
|
||||
% (DefaultFilePath, fileName, jobTemplate, geometryTolerance, curveAccuracy)
|
||||
)
|
||||
def setJobDefaults(jobTemplate, geometryTolerance, curveAccuracy):
|
||||
Path.Log.track("(%s, %s, %s)" % (jobTemplate, geometryTolerance, curveAccuracy))
|
||||
pref = preferences()
|
||||
pref.SetString(DefaultFilePath, fileName)
|
||||
pref.SetString(DefaultJobTemplate, jobTemplate)
|
||||
pref.SetFloat(GeometryTolerance, geometryTolerance)
|
||||
pref.SetFloat(LibAreaCurveAccuracy, curveAccuracy)
|
||||
|
||||
@@ -107,6 +107,12 @@ class AssetManager:
|
||||
visited_uris: Set[AssetUri],
|
||||
depth: Optional[int] = None,
|
||||
) -> Optional[_AssetConstructionData]:
|
||||
# Log library fetch details
|
||||
if uri.asset_type == "library":
|
||||
logger.info(
|
||||
f"LIBRARY FETCH: Loading library '{uri.asset_id}' with depth={depth} from stores {store_names}"
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"_fetch_asset_construction_data_recursive_async called {store_names} {uri} {depth}"
|
||||
)
|
||||
@@ -126,29 +132,59 @@ class AssetManager:
|
||||
# Fetch the requested asset, trying each store in order
|
||||
raw_data = None
|
||||
found_store_name = None
|
||||
|
||||
# Log toolbit search details
|
||||
if uri.asset_type == "toolbit":
|
||||
logger.info(
|
||||
f"TOOLBIT SEARCH: Looking for toolbit '{uri.asset_id}' in stores: {store_names}"
|
||||
)
|
||||
|
||||
for current_store_name in store_names:
|
||||
store = self.stores.get(current_store_name)
|
||||
if not store:
|
||||
logger.warning(f"Store '{current_store_name}' not registered. Skipping.")
|
||||
continue
|
||||
|
||||
# Log store search path for toolbits
|
||||
if uri.asset_type == "toolbit":
|
||||
store_path = getattr(store, "base_path", "unknown")
|
||||
logger.info(
|
||||
f"TOOLBIT SEARCH: Checking store '{current_store_name}' at path: {store_path}"
|
||||
)
|
||||
|
||||
try:
|
||||
raw_data = await store.get(uri)
|
||||
found_store_name = current_store_name
|
||||
if uri.asset_type == "toolbit":
|
||||
logger.info(
|
||||
f"TOOLBIT FOUND: '{uri.asset_id}' found in store '{found_store_name}'"
|
||||
)
|
||||
logger.debug(
|
||||
f"_fetch_asset_construction_data_recursive_async: Asset {uri} found in store {found_store_name}"
|
||||
)
|
||||
break # Asset found, no need to check other stores
|
||||
except FileNotFoundError:
|
||||
if uri.asset_type == "toolbit":
|
||||
logger.info(
|
||||
f"TOOLBIT SEARCH: '{uri.asset_id}' NOT found in store '{current_store_name}'"
|
||||
)
|
||||
logger.debug(
|
||||
f"_fetch_asset_construction_data_recursive_async: Asset {uri} not found in store {current_store_name}"
|
||||
)
|
||||
continue # Try next store
|
||||
|
||||
if raw_data is None or not found_store_name:
|
||||
if uri.asset_type == "toolbit":
|
||||
logger.warning(
|
||||
f"TOOLBIT NOT FOUND: '{uri.asset_id}' not found in any of the stores: {store_names}"
|
||||
)
|
||||
return None # Asset not found in any store
|
||||
|
||||
if depth == 0:
|
||||
if uri.asset_type == "library":
|
||||
logger.warning(
|
||||
f"LIBRARY SHALLOW: Library '{uri.asset_id}' loaded with depth=0 - no dependencies will be resolved"
|
||||
)
|
||||
return _AssetConstructionData(
|
||||
store=found_store_name,
|
||||
uri=uri,
|
||||
@@ -241,10 +277,23 @@ class AssetManager:
|
||||
resolved_dependencies: Optional[Mapping[AssetUri, Any]] = None
|
||||
if construction_data.dependencies_data is not None:
|
||||
resolved_dependencies = {}
|
||||
|
||||
# Log dependency resolution for libraries
|
||||
if construction_data.uri.asset_type == "library":
|
||||
logger.info(
|
||||
f"LIBRARY DEPS: Resolving {len(construction_data.dependencies_data)} dependencies for library '{construction_data.uri.asset_id}'"
|
||||
)
|
||||
|
||||
for (
|
||||
dep_uri,
|
||||
dep_data_node,
|
||||
) in construction_data.dependencies_data.items():
|
||||
# Log toolbit dependency resolution
|
||||
if dep_uri.asset_type == "toolbit":
|
||||
logger.info(
|
||||
f"TOOLBIT DEP: Resolving dependency '{dep_uri.asset_id}' for library '{construction_data.uri.asset_id}'"
|
||||
)
|
||||
|
||||
# Assuming dependencies are fetched from the same store context
|
||||
# for caching purposes. If a dependency *could* be from a
|
||||
# different store and that store has different cacheability,
|
||||
@@ -252,7 +301,18 @@ class AssetManager:
|
||||
# For now, use the parent's store_name_for_cache.
|
||||
try:
|
||||
dep = self._build_asset_tree_from_data_sync(dep_data_node)
|
||||
if dep_uri.asset_type == "toolbit":
|
||||
if dep:
|
||||
logger.info(
|
||||
f"TOOLBIT DEP: Successfully resolved '{dep_uri.asset_id}' -> {type(dep).__name__}"
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"TOOLBIT DEP: Dependency '{dep_uri.asset_id}' resolved to None"
|
||||
)
|
||||
except Exception as e:
|
||||
if dep_uri.asset_type == "toolbit":
|
||||
logger.error(f"TOOLBIT DEP: Error resolving '{dep_uri.asset_id}': {e}")
|
||||
logger.error(
|
||||
f"Error building dependency '{dep_uri}' for asset '{construction_data.uri}': {e}",
|
||||
exc_info=True,
|
||||
@@ -260,9 +320,31 @@ class AssetManager:
|
||||
else:
|
||||
resolved_dependencies[dep_uri] = dep
|
||||
|
||||
# Log final dependency count for libraries
|
||||
if construction_data.uri.asset_type == "library":
|
||||
toolbit_deps = [
|
||||
uri for uri in resolved_dependencies.keys() if uri.asset_type == "toolbit"
|
||||
]
|
||||
logger.info(
|
||||
f"LIBRARY DEPS: Resolved {len(resolved_dependencies)} total dependencies ({len(toolbit_deps)} toolbits) for library '{construction_data.uri.asset_id}'"
|
||||
)
|
||||
else:
|
||||
# Log when dependencies_data is None
|
||||
if construction_data.uri.asset_type == "library":
|
||||
logger.warning(
|
||||
f"LIBRARY NO DEPS: Library '{construction_data.uri.asset_id}' has dependencies_data=None - was loaded with depth=0"
|
||||
)
|
||||
|
||||
asset_class = construction_data.asset_class
|
||||
serializer = self.get_serializer_for_class(asset_class)
|
||||
try:
|
||||
# Log library instantiation with dependency info
|
||||
if construction_data.uri.asset_type == "library":
|
||||
dep_count = len(resolved_dependencies) if resolved_dependencies else 0
|
||||
logger.info(
|
||||
f"LIBRARY INSTANTIATE: Creating library '{construction_data.uri.asset_id}' with {dep_count} dependencies"
|
||||
)
|
||||
|
||||
final_asset = asset_class.from_bytes(
|
||||
construction_data.raw_data,
|
||||
construction_data.uri.asset_id,
|
||||
@@ -307,6 +389,24 @@ class AssetManager:
|
||||
# Log entry with thread info for verification
|
||||
calling_thread_name = threading.current_thread().name
|
||||
stores_list = [store] if isinstance(store, str) else store
|
||||
|
||||
# Log all asset get requests
|
||||
asset_uri_obj = AssetUri(uri) if isinstance(uri, str) else uri
|
||||
if asset_uri_obj.asset_type == "library":
|
||||
logger.info(
|
||||
f"LIBRARY GET: Request for library '{asset_uri_obj.asset_id}' with depth={depth}"
|
||||
)
|
||||
elif asset_uri_obj.asset_type == "toolbit":
|
||||
logger.info(
|
||||
f"TOOLBIT GET: Direct request for toolbit '{asset_uri_obj.asset_id}' with depth={depth} from stores {stores_list}"
|
||||
)
|
||||
# Add stack trace to see who's calling this
|
||||
import traceback
|
||||
|
||||
stack = traceback.format_stack()
|
||||
caller_info = "".join(stack[-3:-1]) # Get the 2 frames before this one
|
||||
logger.info(f"TOOLBIT GET CALLER:\n{caller_info}")
|
||||
|
||||
logger.debug(
|
||||
f"AssetManager.get(uri='{uri}', stores='{stores_list}', depth='{depth}') called from thread: {calling_thread_name}"
|
||||
)
|
||||
|
||||
@@ -29,6 +29,7 @@ from .util import (
|
||||
make_export_filters,
|
||||
get_serializer_from_extension,
|
||||
)
|
||||
import Path.Preferences as Preferences
|
||||
|
||||
|
||||
class AssetOpenDialog(QFileDialog):
|
||||
@@ -40,7 +41,11 @@ class AssetOpenDialog(QFileDialog):
|
||||
parent=None,
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.setDirectory(pathlib.Path.home().as_posix())
|
||||
|
||||
# Set default directory based on asset type
|
||||
default_dir = self._get_default_directory(asset_class)
|
||||
self.setDirectory(default_dir.as_posix())
|
||||
|
||||
self.asset_class = asset_class
|
||||
self.asset_manager = asset_manager
|
||||
self.serializers = list(serializers)
|
||||
@@ -70,7 +75,7 @@ class AssetOpenDialog(QFileDialog):
|
||||
raw_data = file_path.read_bytes()
|
||||
dependencies = serializer_class.extract_dependencies(raw_data)
|
||||
for dependency_uri in dependencies:
|
||||
if not self.asset_manager.exists(dependency_uri):
|
||||
if not self.asset_manager.exists(dependency_uri, store=["local", "builtin"]):
|
||||
QMessageBox.critical(
|
||||
self,
|
||||
"Error",
|
||||
@@ -101,6 +106,24 @@ class AssetOpenDialog(QFileDialog):
|
||||
return file_path, asset
|
||||
return None
|
||||
|
||||
def _get_default_directory(self, asset_class: Type[Asset]) -> pathlib.Path:
|
||||
"""Get the appropriate default directory based on asset type."""
|
||||
try:
|
||||
asset_path = Preferences.getAssetPath()
|
||||
|
||||
# Check asset type to determine subdirectory
|
||||
asset_type = getattr(asset_class, "asset_type", None)
|
||||
if asset_type == "toolbit":
|
||||
return asset_path / "Tool" / "Bit"
|
||||
elif asset_type == "library" or asset_type == "toolbitlibrary":
|
||||
return asset_path / "Tool" / "Library"
|
||||
else:
|
||||
# Default to asset path root for unknown types
|
||||
return asset_path
|
||||
except Exception:
|
||||
# Fallback to home directory if anything goes wrong
|
||||
return pathlib.Path.home()
|
||||
|
||||
|
||||
class AssetSaveDialog(QFileDialog):
|
||||
def __init__(
|
||||
@@ -110,7 +133,10 @@ class AssetSaveDialog(QFileDialog):
|
||||
parent=None,
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.setDirectory(pathlib.Path.home().as_posix())
|
||||
|
||||
# Set default directory based on asset type
|
||||
default_dir = self._get_default_directory(asset_class)
|
||||
self.setDirectory(default_dir.as_posix())
|
||||
self.asset_class = asset_class
|
||||
self.serializers = list(serializers)
|
||||
self.setFileMode(QFileDialog.AnyFile)
|
||||
@@ -145,6 +171,11 @@ class AssetSaveDialog(QFileDialog):
|
||||
QMessageBox.critical(self, "Error", f"Failed to export asset: {e}")
|
||||
return False
|
||||
|
||||
def _get_default_directory(self, asset_class: Type[Asset]) -> pathlib.Path:
|
||||
"""Get the appropriate default directory based on asset type."""
|
||||
# For exports, default to home directory instead of CAM assets path
|
||||
return pathlib.Path.home()
|
||||
|
||||
def exec_(self, asset: Asset) -> Optional[Tuple[pathlib.Path, Type[AssetSerializer]]]:
|
||||
self.setWindowTitle(f"Save {asset.label or self.asset_class.asset_type}")
|
||||
if super().exec_():
|
||||
|
||||
@@ -131,13 +131,13 @@ def ensure_toolbitshape_assets_present(asset_manager: AssetManager, store_name:
|
||||
|
||||
def ensure_toolbitshape_assets_initialized(asset_manager: AssetManager, store_name: str = "local"):
|
||||
"""
|
||||
Copies an example shape to the given store if it is currently empty.
|
||||
Ensures the toolbitshape directory structure exists without adding any files.
|
||||
"""
|
||||
builtin_shape_path = Preferences.getBuiltinShapePath()
|
||||
from pathlib import Path
|
||||
|
||||
if asset_manager.is_empty("toolbitshape", store=store_name):
|
||||
path = builtin_shape_path / "endmill.fcstd"
|
||||
asset_manager.add_file("toolbitshape", path, store=store_name, asset_id="example")
|
||||
# Get the shape directory path and ensure it exists
|
||||
shape_path = Preferences.getAssetPath() / "Tools" / "Shape"
|
||||
shape_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def ensure_assets_initialized(asset_manager: AssetManager, store="local"):
|
||||
@@ -157,6 +157,16 @@ def _on_asset_path_changed(group, key, value):
|
||||
|
||||
# Set up the local CAM asset storage.
|
||||
asset_mapping = {
|
||||
"toolbitlibrary": "Tools/Library/{asset_id}.fctl",
|
||||
"toolbit": "Tools/Bit/{asset_id}.fctb",
|
||||
"toolbitshape": "Tools/Shape/{asset_id}.fcstd",
|
||||
"toolbitshapesvg": "Tools/Shape/{asset_id}", # Asset ID has ".svg" included
|
||||
"toolbitshapepng": "Tools/Shape/{asset_id}", # Asset ID has ".png" included
|
||||
"machine": "Machine/{asset_id}.fcm",
|
||||
}
|
||||
|
||||
# Separate mapping for builtin assets (maintains original structure)
|
||||
builtin_asset_mapping = {
|
||||
"toolbitlibrary": "Library/{asset_id}.fctl",
|
||||
"toolbit": "Bit/{asset_id}.fctb",
|
||||
"toolbitshape": "Shape/{asset_id}.fcstd",
|
||||
@@ -174,7 +184,7 @@ user_asset_store = FileStore(
|
||||
builtin_asset_store = FileStore(
|
||||
name="builtin",
|
||||
base_dir=Preferences.getBuiltinAssetPath(),
|
||||
mapping=asset_mapping,
|
||||
mapping=builtin_asset_mapping,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -35,6 +35,9 @@ class FCTLSerializer(AssetSerializer):
|
||||
extensions = (".fctl",)
|
||||
mime_type = "application/x-freecad-toolbit-library"
|
||||
|
||||
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
|
||||
Path.Log.trackModule(Path.Log.thisModule())
|
||||
|
||||
@classmethod
|
||||
def get_label(cls) -> str:
|
||||
return FreeCAD.Qt.translate("CAM", "FreeCAD Tool Library")
|
||||
@@ -66,6 +69,7 @@ class FCTLSerializer(AssetSerializer):
|
||||
Creates a Library instance from serialized data and resolved
|
||||
dependencies.
|
||||
"""
|
||||
|
||||
data_dict = json.loads(data.decode("utf-8"))
|
||||
# The id parameter from the Asset.from_bytes method is the canonical ID
|
||||
# for the asset being deserialized. We should use this ID for the library
|
||||
@@ -103,9 +107,80 @@ class FCTLSerializer(AssetSerializer):
|
||||
Path.Log.warning(
|
||||
f"Tool with id {tool_id} not found in dependencies during deserialization."
|
||||
)
|
||||
# Create a placeholder toolbit with the original ID to preserve library structure
|
||||
from ...toolbit.models.custom import ToolBitCustom
|
||||
from ...shape.models.custom import ToolBitShapeCustom
|
||||
|
||||
placeholder_shape = ToolBitShapeCustom(tool_id)
|
||||
placeholder_toolbit = ToolBitCustom(placeholder_shape, id=tool_id)
|
||||
placeholder_toolbit.label = f"Missing Tool ({tool_id})"
|
||||
library.add_bit(placeholder_toolbit, bit_no=tool_no)
|
||||
Path.Log.info(f"Created placeholder toolbit with original ID {tool_id}")
|
||||
return library
|
||||
|
||||
@classmethod
|
||||
def deep_deserialize(cls, data: bytes) -> Library:
|
||||
# TODO: attempt to fetch tools from the asset manager here
|
||||
return cls.deserialize(data, str(uuid.uuid4()), {})
|
||||
"""Deep deserialize a library by fetching all toolbit dependencies."""
|
||||
import uuid
|
||||
from ...camassets import cam_assets
|
||||
|
||||
# Generate a unique ID for this library instance
|
||||
library_id = str(uuid.uuid4())
|
||||
|
||||
Path.Log.info(
|
||||
f"FCTL DEEP_DESERIALIZE: Starting deep deserialization for library id='{library_id}'"
|
||||
)
|
||||
|
||||
# Extract dependency URIs from the library data
|
||||
dependency_uris = cls.extract_dependencies(data)
|
||||
Path.Log.info(
|
||||
f"FCTL DEEP_DESERIALIZE: Found {len(dependency_uris)} toolbit dependencies: {[uri.asset_id for uri in dependency_uris]}"
|
||||
)
|
||||
|
||||
# Fetch all toolbit dependencies
|
||||
resolved_dependencies = {}
|
||||
for dep_uri in dependency_uris:
|
||||
try:
|
||||
Path.Log.info(
|
||||
f"FCTL DEEP_DESERIALIZE: Fetching toolbit '{dep_uri.asset_id}' from stores ['local', 'builtin']"
|
||||
)
|
||||
|
||||
# Check if toolbit exists in each store individually for debugging
|
||||
exists_local = cam_assets.exists(dep_uri, store="local")
|
||||
exists_builtin = cam_assets.exists(dep_uri, store="builtin")
|
||||
Path.Log.info(
|
||||
f"FCTL DEEP_DESERIALIZE: Toolbit '{dep_uri.asset_id}' exists - local: {exists_local}, builtin: {exists_builtin}"
|
||||
)
|
||||
|
||||
toolbit = cam_assets.get(dep_uri, store=["local", "builtin"], depth=0)
|
||||
resolved_dependencies[dep_uri] = toolbit
|
||||
Path.Log.info(
|
||||
f"FCTL DEEP_DESERIALIZE: Successfully fetched toolbit '{dep_uri.asset_id}'"
|
||||
)
|
||||
except Exception as e:
|
||||
Path.Log.warning(
|
||||
f"FCTL DEEP_DESERIALIZE: Failed to fetch toolbit '{dep_uri.asset_id}': {e}"
|
||||
)
|
||||
|
||||
# Try to get more detailed error information
|
||||
try:
|
||||
# Check what's actually in the stores
|
||||
local_toolbits = cam_assets.list_assets("toolbit", store="local")
|
||||
local_ids = [uri.asset_id for uri in local_toolbits]
|
||||
Path.Log.info(
|
||||
f"FCTL DEBUG: Local store has {len(local_ids)} toolbits: {local_ids[:10]}{'...' if len(local_ids) > 10 else ''}"
|
||||
)
|
||||
|
||||
if dep_uri.asset_id in local_ids:
|
||||
Path.Log.warning(
|
||||
f"FCTL DEBUG: Toolbit '{dep_uri.asset_id}' IS in local store list but get() failed!"
|
||||
)
|
||||
except Exception as list_error:
|
||||
Path.Log.error(f"FCTL DEBUG: Failed to list local toolbits: {list_error}")
|
||||
|
||||
Path.Log.info(
|
||||
f"FCTL DEEP_DESERIALIZE: Resolved {len(resolved_dependencies)} of {len(dependency_uris)} dependencies"
|
||||
)
|
||||
|
||||
# Now deserialize with the resolved dependencies
|
||||
return cls.deserialize(data, library_id, resolved_dependencies)
|
||||
|
||||
@@ -284,6 +284,7 @@ class LibraryBrowserWidget(ToolBitBrowserWidget):
|
||||
|
||||
selected_items = self._tool_list_widget.selectedItems()
|
||||
has_selection = bool(selected_items)
|
||||
has_library = self.current_library is not None
|
||||
|
||||
# Add actions in the desired order
|
||||
edit_action = context_menu.addAction("Edit", self._on_edit_requested)
|
||||
@@ -310,13 +311,17 @@ class LibraryBrowserWidget(ToolBitBrowserWidget):
|
||||
|
||||
context_menu.addSeparator()
|
||||
|
||||
action = context_menu.addAction(
|
||||
"Remove from Library", self._on_remove_from_library_requested
|
||||
)
|
||||
action.setShortcut(QtGui.QKeySequence.Delete)
|
||||
# Only show "Remove from Library" when viewing a specific library
|
||||
if has_library:
|
||||
action = context_menu.addAction(
|
||||
"Remove from Library", self._on_remove_from_library_requested
|
||||
)
|
||||
action.setShortcut(QtGui.QKeySequence.Delete)
|
||||
|
||||
action = context_menu.addAction("Delete from disk", self._on_delete_requested)
|
||||
action.setShortcut(QtGui.QKeySequence("Shift+Delete"))
|
||||
# Only show "Delete from disk" when viewing 'all tools' (no library selected)
|
||||
if not has_library:
|
||||
action = context_menu.addAction("Delete from disk", self._on_delete_requested)
|
||||
action.setShortcut(QtGui.QKeySequence("Shift+Delete"))
|
||||
|
||||
# Execute the menu
|
||||
context_menu.exec_(self._tool_list_widget.mapToGlobal(position))
|
||||
@@ -443,14 +448,29 @@ class LibraryBrowserWidget(ToolBitBrowserWidget):
|
||||
|
||||
toolbit_data_bytes = toolbit_yaml_str.encode("utf-8")
|
||||
toolbit = YamlToolBitSerializer.deserialize(toolbit_data_bytes, dependencies=None)
|
||||
# Assign a new tool id and a label
|
||||
toolbit.set_id()
|
||||
self._asset_manager.add(toolbit) # Save the new toolbit to disk
|
||||
|
||||
# Add the bit to the current library
|
||||
added_toolbit = current_library.add_bit(toolbit)
|
||||
# Get the original toolbit ID from the deserialized data
|
||||
original_id = toolbit.id
|
||||
Path.Log.info(f"COPY PASTE: Attempting to paste toolbit with original_id={original_id}")
|
||||
|
||||
# Check if toolbit already exists in asset manager
|
||||
toolbit_uri = toolbit.get_uri()
|
||||
existing_toolbit = None
|
||||
try:
|
||||
existing_toolbit = self._asset_manager.get(
|
||||
toolbit_uri, store=["local", "builtin"], depth=0
|
||||
)
|
||||
Path.Log.info(f"COPY PASTE: Found existing toolbit {original_id}, using reference")
|
||||
except FileNotFoundError:
|
||||
# Toolbit doesn't exist, save it as new
|
||||
Path.Log.info(f"COPY PASTE: Toolbit {original_id} not found, creating new one")
|
||||
self._asset_manager.add(toolbit)
|
||||
existing_toolbit = toolbit
|
||||
|
||||
# Add the existing or new toolbit to the current library
|
||||
added_toolbit = current_library.add_bit(existing_toolbit)
|
||||
if added_toolbit:
|
||||
new_uris.add(str(toolbit.get_uri()))
|
||||
new_uris.add(str(existing_toolbit.get_uri()))
|
||||
|
||||
if new_uris:
|
||||
self._asset_manager.add(current_library) # Save the modified library
|
||||
@@ -485,16 +505,25 @@ class LibraryBrowserWidget(ToolBitBrowserWidget):
|
||||
|
||||
toolbit_data_bytes = toolbit_yaml_str.encode("utf-8")
|
||||
toolbit = YamlToolBitSerializer.deserialize(toolbit_data_bytes, dependencies=None)
|
||||
source_library.remove_bit(toolbit)
|
||||
|
||||
# Remove it from the old library, add it to the new library
|
||||
source_library.remove_bit(toolbit)
|
||||
added_toolbit = current_library.add_bit(toolbit)
|
||||
if added_toolbit:
|
||||
new_uris.add(str(toolbit.get_uri()))
|
||||
# Get the original toolbit ID and find the existing toolbit
|
||||
original_id = toolbit.id
|
||||
Path.Log.info(f"CUT PASTE: Moving toolbit with original_id={original_id}")
|
||||
|
||||
# The toolbit itself does not change, so we don't need to save it.
|
||||
# It is only the reference in the library that changes.
|
||||
toolbit_uri = toolbit.get_uri()
|
||||
try:
|
||||
existing_toolbit = self._asset_manager.get(
|
||||
toolbit_uri, store=["local", "builtin"], depth=0
|
||||
)
|
||||
Path.Log.info(f"CUT PASTE: Found existing toolbit {original_id}, using reference")
|
||||
|
||||
# Remove from source library, add to target library
|
||||
source_library.remove_bit(existing_toolbit)
|
||||
added_toolbit = current_library.add_bit(existing_toolbit)
|
||||
if added_toolbit:
|
||||
new_uris.add(str(existing_toolbit.get_uri()))
|
||||
except FileNotFoundError:
|
||||
Path.Log.warning(f"CUT PASTE: Toolbit {original_id} not found in asset manager")
|
||||
|
||||
if new_uris:
|
||||
# Save the modified libraries
|
||||
|
||||
@@ -350,7 +350,9 @@ class LibraryEditor(QWidget):
|
||||
self.form.renameLibraryButton.setEnabled(library_selected)
|
||||
self.form.exportLibraryButton.setEnabled(library_selected)
|
||||
self.form.importLibraryButton.setEnabled(True)
|
||||
self.form.addToolBitButton.setEnabled(library_selected)
|
||||
self.form.addToolBitButton.setEnabled(
|
||||
True
|
||||
) # Always enabled - can create standalone toolbits
|
||||
# TODO: self.form.exportToolBitButton.setEnabled(toolbit_selected)
|
||||
|
||||
def _save_library(self):
|
||||
@@ -475,17 +477,9 @@ class LibraryEditor(QWidget):
|
||||
self._update_button_states()
|
||||
|
||||
def _on_add_toolbit_requested(self):
|
||||
"""Handles request to add a new toolbit to the current library."""
|
||||
"""Handles request to add a new toolbit to the current library or create standalone."""
|
||||
Path.Log.debug("_on_add_toolbit_requested: Called.")
|
||||
current_library = self.browser.get_current_library()
|
||||
if not current_library:
|
||||
Path.Log.warning("Cannot add toolbit: No library selected.")
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
FreeCAD.Qt.translate("CAM", "Warning"),
|
||||
FreeCAD.Qt.translate("CAM", "Please select a library first."),
|
||||
)
|
||||
return
|
||||
|
||||
# Select the shape for the new toolbit
|
||||
selector = ShapeSelector()
|
||||
@@ -508,15 +502,19 @@ class LibraryEditor(QWidget):
|
||||
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
|
||||
toolno = current_library.add_bit(new_toolbit)
|
||||
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}."
|
||||
)
|
||||
|
||||
# Save the library
|
||||
cam_assets.add(current_library)
|
||||
# Add the toolbit to the current library if one is selected
|
||||
if current_library:
|
||||
toolno = current_library.add_bit(new_toolbit)
|
||||
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}."
|
||||
)
|
||||
# Save the library
|
||||
cam_assets.add(current_library)
|
||||
else:
|
||||
Path.Log.debug(
|
||||
f"_on_add_toolbit_requested: Created standalone toolbit {new_toolbit.get_id()} (URI: {new_toolbit.get_uri()})"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
Path.Log.error(f"Failed to create or add new toolbit: {e}")
|
||||
@@ -552,17 +550,50 @@ class LibraryEditor(QWidget):
|
||||
return
|
||||
file_path, toolbit = cast(Tuple[pathlib.Path, ToolBit], response)
|
||||
|
||||
# Add the imported toolbit to the current library
|
||||
added_toolbit = current_library.add_bit(toolbit)
|
||||
# Debug logging for imported toolbit
|
||||
Path.Log.info(
|
||||
f"IMPORT TOOLBIT: file_path={file_path}, toolbit.id={toolbit.id}, toolbit.label={toolbit.label}"
|
||||
)
|
||||
import traceback
|
||||
|
||||
stack = traceback.format_stack()
|
||||
caller_info = "".join(stack[-3:-1])
|
||||
Path.Log.info(f"IMPORT TOOLBIT CALLER:\n{caller_info}")
|
||||
|
||||
# Check if toolbit already exists in asset manager
|
||||
toolbit_uri = toolbit.get_uri()
|
||||
Path.Log.info(f"IMPORT CHECK: toolbit_uri={toolbit_uri}")
|
||||
existing_toolbit = None
|
||||
try:
|
||||
existing_toolbit = cam_assets.get(toolbit_uri, store=["local", "builtin"], depth=0)
|
||||
Path.Log.info(
|
||||
f"IMPORT CHECK: Toolbit {toolbit.id} already exists, using existing reference"
|
||||
)
|
||||
Path.Log.info(
|
||||
f"IMPORT CHECK: existing_toolbit.id={existing_toolbit.id}, existing_toolbit.label={existing_toolbit.label}"
|
||||
)
|
||||
except FileNotFoundError:
|
||||
# Toolbit doesn't exist, save it as new
|
||||
Path.Log.info(f"IMPORT CHECK: Toolbit {toolbit.id} is new, saving to disk")
|
||||
new_uri = cam_assets.add(toolbit)
|
||||
Path.Log.info(f"IMPORT CHECK: Toolbit saved with new URI: {new_uri}")
|
||||
existing_toolbit = toolbit
|
||||
|
||||
# Add the toolbit (existing or new) to the current library
|
||||
Path.Log.info(
|
||||
f"IMPORT ADD: Adding toolbit {existing_toolbit.id} to library {current_library.label}"
|
||||
)
|
||||
added_toolbit = current_library.add_bit(existing_toolbit)
|
||||
if added_toolbit:
|
||||
cam_assets.add(toolbit) # Save the imported toolbit to disk
|
||||
Path.Log.info(f"IMPORT ADD: Successfully added toolbit to library")
|
||||
cam_assets.add(current_library) # Save the modified library
|
||||
self.browser.refresh()
|
||||
self.browser.select_by_uri([str(toolbit.get_uri())])
|
||||
self.browser.select_by_uri([str(existing_toolbit.get_uri())])
|
||||
self._update_button_states()
|
||||
else:
|
||||
Path.Log.warning(f"IMPORT ADD: Failed to add toolbit {existing_toolbit.id} to library")
|
||||
Path.Log.warning(
|
||||
f"Failed to import toolbit from {file_path} to library {current_library.label}."
|
||||
f"IMPORT FAILED: Failed to import toolbit from {file_path} to library {current_library.label}."
|
||||
)
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
|
||||
@@ -42,20 +42,32 @@ class LibraryPropertyDialog(QtWidgets.QDialog):
|
||||
self.form.lineEditLibraryName.setText(self.library.label)
|
||||
self.update_window_title()
|
||||
|
||||
if new:
|
||||
label = FreeCAD.Qt.translate("CAM", "Create Library")
|
||||
self.form.pushButtonSave.setText(label)
|
||||
|
||||
self.form.buttonBox.accepted.connect(self.accept)
|
||||
self.form.buttonBox.accepted.connect(self.save_properties)
|
||||
self.form.buttonBox.rejected.connect(self.reject)
|
||||
self.form.pushButtonSave.clicked.connect(self.save_properties)
|
||||
|
||||
# Connect text changed signal to update window title
|
||||
self.form.lineEditLibraryName.textChanged.connect(self.update_window_title)
|
||||
|
||||
# Make the OK button the default so Enter key works
|
||||
ok_button = self.form.buttonBox.button(QtWidgets.QDialogButtonBox.Ok)
|
||||
cancel_button = self.form.buttonBox.button(QtWidgets.QDialogButtonBox.Cancel)
|
||||
|
||||
if cancel_button:
|
||||
cancel_button.setDefault(False)
|
||||
cancel_button.setAutoDefault(False)
|
||||
|
||||
if ok_button:
|
||||
ok_button.setDefault(True)
|
||||
ok_button.setAutoDefault(True)
|
||||
ok_button.setFocus() # Also set focus to the OK button
|
||||
|
||||
# Set minimum width for the dialog
|
||||
self.setMinimumWidth(450)
|
||||
|
||||
# Set focus to the text input so user can start typing immediately
|
||||
self.form.lineEditLibraryName.setFocus()
|
||||
self.form.lineEditLibraryName.selectAll() # Select all text for easy replacement
|
||||
|
||||
def update_window_title(self):
|
||||
# Update title based on current text in the line edit
|
||||
current_name = self.form.lineEditLibraryName.text()
|
||||
|
||||
@@ -28,7 +28,7 @@ from ...shape import ToolBitShape
|
||||
from ..models.base import ToolBit
|
||||
|
||||
|
||||
if False:
|
||||
if True:
|
||||
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
|
||||
Path.Log.trackModule(Path.Log.thisModule())
|
||||
else:
|
||||
@@ -100,15 +100,19 @@ class FCTBSerializer(AssetSerializer):
|
||||
f"is not a ToolBitShape instance. {dependencies}"
|
||||
)
|
||||
|
||||
# Find the correct ToolBit subclass for the shape
|
||||
Path.Log.debug(
|
||||
f"FCTBSerializer.deserialize: shape = {shape!r}, id = {id!r},"
|
||||
f" params = {shape.get_parameters()}, attrs = {attrs!r}"
|
||||
)
|
||||
return ToolBit.from_shape(shape, attrs, id)
|
||||
|
||||
@classmethod
|
||||
def deep_deserialize(cls, data: bytes) -> ToolBit:
|
||||
"""Deep deserialize preserving the original toolbit ID."""
|
||||
|
||||
attrs_map = json.loads(data)
|
||||
original_id = attrs_map.get("id")
|
||||
|
||||
asset_class = cast(ToolBit, cls.for_class)
|
||||
return asset_class.from_dict(attrs_map)
|
||||
toolbit = asset_class.from_dict(attrs_map)
|
||||
|
||||
if original_id:
|
||||
toolbit.id = original_id # Preserve the original ID
|
||||
|
||||
return toolbit
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
import yaml
|
||||
from typing import List, Optional, Mapping, Type
|
||||
from typing import List, Optional, Mapping, Type, cast
|
||||
from ...assets.serializer import AssetSerializer
|
||||
from ...assets.uri import AssetUri
|
||||
from ...shape import ToolBitShape
|
||||
@@ -81,8 +81,13 @@ class YamlToolBitSerializer(AssetSerializer):
|
||||
|
||||
@classmethod
|
||||
def deep_deserialize(cls, data: bytes) -> ToolBit:
|
||||
"""
|
||||
Like deserialize(), but builds dependencies itself if they are
|
||||
sufficiently defined in the data.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
"""Deep deserialize preserving the original toolbit ID."""
|
||||
data_dict = yaml.safe_load(data)
|
||||
if not isinstance(data_dict, dict):
|
||||
raise ValueError("Invalid YAML data for ToolBit")
|
||||
|
||||
original_id = data_dict.get("id") # Extract the original ID
|
||||
toolbit = ToolBit.from_dict(data_dict)
|
||||
if original_id:
|
||||
toolbit.id = original_id # Preserve the original ID
|
||||
return toolbit
|
||||
|
||||
@@ -395,7 +395,7 @@ class ToolBitBrowserWidget(QtGui.QWidget):
|
||||
self._to_clipboard(uris, mode="copy")
|
||||
|
||||
def _on_delete_requested(self):
|
||||
"""Deletes selected toolbits."""
|
||||
"""Deletes selected toolbits and removes them from all libraries."""
|
||||
Path.Log.debug("ToolBitBrowserWidget._on_delete_requested: Function entered.")
|
||||
uris = self.get_selected_bit_uris()
|
||||
if not uris:
|
||||
@@ -406,7 +406,10 @@ class ToolBitBrowserWidget(QtGui.QWidget):
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
FreeCAD.Qt.translate("CAM", "Confirm Deletion"),
|
||||
FreeCAD.Qt.translate("CAM", "Are you sure you want to delete the selected toolbit(s)?"),
|
||||
FreeCAD.Qt.translate(
|
||||
"CAM",
|
||||
"Are you sure you want to delete the selected toolbit(s)? This is not reversible. The toolbits will be removed from disk and from all libraries that contain them.",
|
||||
),
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No,
|
||||
)
|
||||
@@ -415,19 +418,66 @@ class ToolBitBrowserWidget(QtGui.QWidget):
|
||||
return
|
||||
|
||||
deleted_count = 0
|
||||
libraries_modified = [] # Use list instead of set since Library objects aren't hashable
|
||||
|
||||
for uri_string in uris:
|
||||
try:
|
||||
# Delete the toolbit using the asset manager
|
||||
self._asset_manager.delete(AssetUri(uri_string))
|
||||
toolbit_uri = AssetUri(uri_string)
|
||||
|
||||
# First, remove the toolbit from all libraries that contain it
|
||||
libraries_to_update = self._find_libraries_containing_toolbit(toolbit_uri)
|
||||
for library in libraries_to_update:
|
||||
library.remove_bit_by_uri(uri_string)
|
||||
if library not in libraries_modified: # Avoid duplicates
|
||||
libraries_modified.append(library)
|
||||
Path.Log.info(
|
||||
f"Removed toolbit {toolbit_uri.asset_id} from library {library.label}"
|
||||
)
|
||||
|
||||
# Then delete the toolbit file from disk
|
||||
self._asset_manager.delete(toolbit_uri)
|
||||
deleted_count += 1
|
||||
Path.Log.info(f"Deleted toolbit file {uri_string}")
|
||||
|
||||
except Exception as e:
|
||||
Path.Log.error(f"Failed to delete toolbit {uri_string}: {e}")
|
||||
# Optionally show a message box to the user
|
||||
|
||||
# Save all modified libraries
|
||||
for library in libraries_modified:
|
||||
try:
|
||||
self._asset_manager.add(library)
|
||||
Path.Log.info(f"Saved updated library {library.label}")
|
||||
except Exception as e:
|
||||
Path.Log.error(f"Failed to save library {library.label}: {e}")
|
||||
|
||||
if deleted_count > 0:
|
||||
Path.Log.info(f"Deleted {deleted_count} toolbit(s).")
|
||||
Path.Log.info(
|
||||
f"Deleted {deleted_count} toolbit(s) and updated {len(libraries_modified)} libraries."
|
||||
)
|
||||
self.refresh()
|
||||
|
||||
def _find_libraries_containing_toolbit(self, toolbit_uri: AssetUri) -> List:
|
||||
"""Find all libraries that contain the specified toolbit."""
|
||||
from ...library.models.library import Library
|
||||
|
||||
libraries_with_toolbit = []
|
||||
try:
|
||||
# Get all libraries from the asset manager
|
||||
all_libraries = self._asset_manager.fetch("toolbitlibrary", store="local", depth=1)
|
||||
|
||||
for library in all_libraries:
|
||||
if isinstance(library, Library):
|
||||
# Check if this library contains the toolbit
|
||||
for toolbit in library:
|
||||
if toolbit.get_uri() == toolbit_uri:
|
||||
libraries_with_toolbit.append(library)
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
Path.Log.error(f"Error finding libraries containing toolbit {toolbit_uri}: {e}")
|
||||
|
||||
return libraries_with_toolbit
|
||||
|
||||
def get_selected_bit_uris(self) -> List[str]:
|
||||
"""
|
||||
Returns a list of URIs for the currently selected ToolBit items.
|
||||
|
||||
Reference in New Issue
Block a user