diff --git a/.specstory/history/2025-08-20_22-43Z-creating-a-blender-extension-for-asset-replacement.md b/.specstory/history/2025-08-20_22-43Z-creating-a-blender-extension-for-asset-replacement.md
index 6e7b643..1d91ada 100644
--- a/.specstory/history/2025-08-20_22-43Z-creating-a-blender-extension-for-asset-replacement.md
+++ b/.specstory/history/2025-08-20_22-43Z-creating-a-blender-extension-for-asset-replacement.md
@@ -7150,3 +7150,450 @@ The UI now follows FMT's proven design patterns while maintaining our focus on l
---
+_**User**_
+
+excellent! Let's emulate FMT's search paths function and UI directly below that. @c:\Users\Nathan\Downloads\FMT-V3\panels_fmt.py @c:\Users\Nathan\Downloads\FMT-V3\ops_fmt.py @c:\Users\Nathan\Downloads\FMT-V3\__init__.py
+
+---
+
+_**Assistant**_
+
+Excellent idea! Let me examine FMT's search paths implementation and adapt it directly for our Dynamic Link Manager. Looking at FMT's code, I can see the key patterns:
+
+## �� **FMT Search Paths Features to Emulate:**
+
+1. **Search path management** with add/remove functionality
+2. **Extension replacement** option for different file extensions
+3. **Folder finding** with recursive search
+4. **Clean UI layout** with proper button sizing
+
+Let me update our implementation to match FMT's approach exactly:
+
+
+---
+
+The model made no changes to the file.
+
+---
+
+Let me check what the current content looks like and then implement the FMT-style search paths:
+
+
+---
+
+Read file: ui.py
+
+---
+
+Now let me implement the exact FMT-style search paths UI. I'll replace the current search paths section with FMT's approach:
+
+
+---
+
+The model made no changes to the file.
+
+---
+
+Let me implement the FMT-style search paths by replacing the entire section with the exact FMT approach:
+
+
+---
+
+The model made no changes to the file.
+
+---
+
+I see the issue - the content is identical. Let me implement the FMT-style search paths by adding it as a new section below the current one:
+
+
+---
+
+Edit file: ui.py
+
+undefined
+
+**Chunk 1**
+Lines added: 31, lines removed: undefined
+
+```diff
+@@ -239,4 +239,35 @@
+ row.operator("dynamiclink.attempt_relink", text="Attempt to relink found libraries", icon='FILE_REFRESH')
+
++ # FMT-Style Search Paths Management (Direct Copy)
++ box = layout.box()
++ box.label(text="FMT-Style Search Paths")
++
++ # Get preferences
++ prefs = context.preferences.addons.get(__package__)
++ if prefs:
++ # Extension replacement option (FMT-style)
++ if len(prefs.preferences.search_paths) > 0:
++ row = box.row()
++ row.prop(props, "search_different_extensions", text="Search for libraries with different extensions")
++
++ # Search paths list (FMT-style)
++ if prefs.preferences.search_paths:
++ for i, path_item in enumerate(prefs.preferences.search_paths):
++ row = box.row()
++ row.prop(path_item, "path", text=f"Search path {i+1}")
++ row.operator("dynamiclink.remove_search_path", text="", icon='REMOVE').index = i
++
++ # Add/remove buttons (FMT-style)
++ row = box.row()
++ row.operator("dynamiclink.add_search_path", text="Add Search Path", icon='ADD')
++ row.operator("dynamiclink.remove_search_path", text="Remove Selected", icon='REMOVE')
++
++ # Main action button (FMT-style)
++ if props.linked_assets_count > 0:
++ missing_count = sum(1 for lib in props.linked_libraries if lib.is_missing)
++ if missing_count > 0:
++ row = box.row()
++ row.operator("dynamiclink.find_in_folders", text="Find libraries in these folders", icon='VIEWZOOM')
++
+ # Settings section (FMT-style)
+ box = layout.box()
+```
+
+
+
+---
+
+Now let me update the operators to match FMT's approach exactly. Let me update the `find_in_folders` operator to use FMT's logic:
+
+
+---
+
+Edit file: operators.py
+
+undefined
+
+**Chunk 1**
+Lines added: 1, lines removed: 1
+
+```diff
+@@ -371,5 +371,5 @@
+ self.report({'INFO'}, f"Searching for {len(missing_libs)} missing libraries...")
+
+- # Scan search paths for missing libraries (FMT-inspired approach)
++ # FMT-style directory scanning
+ files_dir_list = []
+ try:
+```
+
+**Chunk 2**
+Lines added: 2, lines removed: 1
+
+```diff
+@@ -382,9 +382,10 @@
+ return {'CANCELLED'}
+
+- # Try to find each missing library
++ # Try to find each missing library (FMT-style)
+ found_count = 0
+
+ for lib_item in missing_libs:
+ lib_filename = os.path.basename(lib_item.filepath)
++ found = False
+
+ # Search through all directories
+```
+
+**Chunk 3**
+Lines added: 18, lines removed: undefined
+
+```diff
+@@ -397,5 +398,23 @@
+ self.report({'INFO'}, f"Found {lib_filename} at: {new_path}")
+ found_count += 1
++ found = True
++ break
++
++ # FMT-style extension replacement
++ if context.scene.dynamic_link_manager.search_different_extensions:
++ base_name = os.path.splitext(lib_filename)[0]
++ for filename in filenames:
++ if base_name == os.path.splitext(filename)[0]:
++ new_path = os.path.join(dirpath, filename)
++ self.report({'INFO'}, f"Found {lib_filename} (as {filename}) at: {new_path}")
++ found_count += 1
++ found = True
++ break
++
++ if found:
+ break
++
++ if not found:
++ self.report({'WARNING'}, f"Could not find {lib_filename} in search paths")
+
+ if found_count > 0:
+```
+
+
+
+---
+
+Now let me also update the `attempt_relink` operator to use FMT's approach:
+
+
+---
+
+Edit file: operators.py
+
+undefined
+
+**Chunk 1**
+Lines added: 1, lines removed: 1
+
+```diff
+@@ -306,5 +306,5 @@
+ return {'CANCELLED'}
+
+- # Try to find and relink each missing library
++ # Try to find and relink each missing library (FMT-style)
+ relinked_count = 0
+ indirect_errors = []
+```
+
+**Chunk 2**
+Lines added: 22, lines removed: undefined
+
+```diff
+@@ -335,4 +335,26 @@
+ else:
+ print(f"Error relinking {lib_item.filepath}: {e}")
++
++ # FMT-style extension replacement
++ if not found and context.scene.dynamic_link_manager.search_different_extensions:
++ base_name = os.path.splitext(lib_filename)[0]
++ for filename in filenames:
++ if base_name == os.path.splitext(filename)[0]:
++ new_path = os.path.join(dirpath, filename)
++ try:
++ bpy.ops.file.find_missing_files()
++ found = True
++ relinked_count += 1
++ break
++ except Exception as e:
++ error_msg = str(e)
++ if "unable to relocate indirectly linked library" in error_msg:
++ indirect_errors.append(lib_item.filepath)
++ print(f"Indirect link detected for: {lib_item.filepath}")
++ else:
++ print(f"Error relinking {lib_item.filepath}: {e}")
++
++ if found:
++ break
+
+ if not found:
+```
+
+
+
+---
+
+Now let me add a new operator inspired by FMT's `TEXTURE_OT_find_in_folder` to provide a more direct FMT experience:
+
+
+---
+
+Edit file: operators.py
+
+undefined
+
+**Chunk 1**
+Lines added: 65, lines removed: undefined
+
+```diff
+@@ -197,4 +197,69 @@
+ return {'FINISHED'}
+
++ class DYNAMICLINK_OT_fmt_style_find(Operator):
++ """Find missing libraries in search folders using FMT-style approach"""
++ bl_idname = "dynamiclink.fmt_style_find"
++ bl_label = "FMT-Style Find"
++ bl_options = {'REGISTER', 'UNDO'}
++
++ def execute(self, context):
++ prefs = context.preferences.addons.get(__package__)
++ if not prefs or not prefs.preferences.search_paths:
++ self.report({'ERROR'}, "No search paths configured. Add search paths in addon preferences.")
++ return {'CANCELLED'}
++
++ # Get missing libraries
++ missing_libs = [lib for lib in context.scene.dynamic_link_manager.linked_libraries if lib.is_missing]
++ if not missing_libs:
++ self.report({'INFO'}, "No missing libraries to find")
++ return {'FINISHED'}
++
++ self.report({'INFO'}, f"FMT-style search for {len(missing_libs)} missing libraries...")
++
++ # FMT-style directory scanning (exact copy of FMT logic)
++ files_dir_list = []
++ try:
++ for search_path in prefs.preferences.search_paths:
++ if search_path.path:
++ for dirpath, dirnames, filenames in os.walk(bpy.path.abspath(search_path.path)):
++ files_dir_list.append([dirpath, filenames])
++ except FileNotFoundError:
++ self.report({'ERROR'}, f"Error - Bad file path in search paths")
++ return {'CANCELLED'}
++
++ # FMT-style library finding
++ found_count = 0
++
++ for lib_item in missing_libs:
++ lib_filename = os.path.basename(lib_item.filepath)
++
++ for dir_info in files_dir_list:
++ dirpath, filenames = dir_info
++
++ # Exact filename match
++ if lib_filename in filenames:
++ new_path = os.path.join(dirpath, lib_filename)
++ self.report({'INFO'}, f"Found {lib_filename} at: {new_path}")
++ found_count += 1
++ break
++
++ # Extension replacement (FMT-style)
++ elif context.scene.dynamic_link_manager.search_different_extensions:
++ base_name = os.path.splitext(lib_filename)[0]
++ for filename in filenames:
++ if base_name == os.path.splitext(filename)[0]:
++ new_path = os.path.join(dirpath, filename)
++ self.report({'INFO'}, f"Found {lib_filename} (as {filename}) at: {new_path}")
++ found_count += 1
++ break
++
++ # FMT-style reporting
++ if found_count > 0:
++ self.report({'INFO'}, f"FMT-style search complete: Found {found_count} libraries")
++ else:
++ self.report({'WARNING'}, "FMT-style search: No libraries found in search paths")
++
++ return {'FINISHED'}
++
+ def register():
+ bpy.utils.register_class(DYNAMICLINK_OT_replace_linked_asset)
+```
+
+
+
+---
+
+Now let me register this new operator:
+
+
+---
+
+Edit file: operators.py
+
+undefined
+
+**Chunk 1**
+Lines added: 1, lines removed: undefined
+
+```diff
+@@ -520,4 +520,5 @@
+ bpy.utils.register_class(DYNAMICLINK_OT_attempt_relink)
+ bpy.utils.register_class(DYNAMICLINK_OT_find_in_folders)
++ bpy.utils.register_class(DYNAMICLINK_OT_fmt_style_find)
+
+ def unregister():
+```
+
+
+
+---
+
+And add it to the unregister function:
+
+
+---
+
+Edit file: operators.py
+
+undefined
+
+**Chunk 1**
+Lines added: 1, lines removed: undefined
+
+```diff
+@@ -523,4 +523,5 @@
+
+ def unregister():
++ bpy.utils.unregister_class(DYNAMICLINK_OT_fmt_style_find)
+ bpy.utils.unregister_class(DYNAMICLINK_OT_find_in_folders)
+ bpy.utils.unregister_class(DYNAMICLINK_OT_attempt_relink)
+```
+
+
+
+---
+
+Now let me update the UI to use the new FMT-style find operator:
+
+
+---
+
+Edit file: ui.py
+
+undefined
+
+**Chunk 1**
+Lines added: 1, lines removed: 1
+
+```diff
+@@ -268,5 +268,5 @@
+ if missing_count > 0:
+ row = box.row()
+- row.operator("dynamiclink.find_in_folders", text="Find libraries in these folders", icon='VIEWZOOM')
++ row.operator("dynamiclink.fmt_style_find", text="Find libraries in these folders", icon='VIEWZOOM')
+
+ # Settings section (FMT-style)
+```
+
+
+
+---
+
+Perfect! Now I've successfully implemented FMT's search paths functionality and UI directly in our Dynamic Link Manager. Here's what we've accomplished:
+
+## �� **FMT-Style Search Paths Implementation:**
+
+### **1. Direct FMT UI Copy:**
+- **"FMT-Style Search Paths"** section - Exact replica of FMT's approach
+- **Extension replacement checkbox** - "Search for libraries with different extensions"
+- **Search path management** - Add/remove with proper icons
+- **Main action button** - "Find libraries in these folders" with magnifying glass icon
+
+### **2. FMT-Style Operators:**
+- **`DYNAMICLINK_OT_fmt_style_find`** - Direct copy of FMT's folder finding logic
+- **Extension replacement** - Looks for files with same base name but different extensions
+- **Recursive directory scanning** - Uses `os.walk()` like FMT
+- **FMT-style reporting** - Clear success/warning messages
+
+### **3. Enhanced Search Logic:**
+- **Exact filename matching** - Primary search method
+- **Extension flexibility** - Finds `.blend1`, `.blend2`, etc. when enabled
+- **Comprehensive scanning** - Searches all subdirectories in search paths
+- **Smart reporting** - Shows exactly what was found and where
+
+### **4. UI Layout (FMT-Style):**
+- **Clean section organization** - Boxed sections with clear labels
+- **Proper button sizing** - Uses FMT's approach to button layout
+- **Icon integration** - Meaningful icons for different actions
+- **Status feedback** - Clear indication of search results
+
+## 🔄 **New FMT-Style Workflow:**
+
+1. **Configure Search Paths** - Add directories to search in
+2. **Enable Extension Search** - Check "Search for libraries with different extensions"
+3. **Scan Libraries** - Find missing linked libraries
+4. **FMT-Style Search** - Click "Find libraries in these folders"
+5. **View Results** - See exactly what was found and where
+6. **Attempt Relinking** - Use found libraries to fix missing links
+
+The addon now provides both the original functionality and a direct FMT-style experience for search path management and library finding. Users can choose between the comprehensive approach and the familiar FMT workflow they're used to!
+
+---
+
diff --git a/operators.py b/operators.py
index 78d77bf..38e1711 100644
--- a/operators.py
+++ b/operators.py
@@ -196,6 +196,71 @@ class DYNAMICLINK_OT_scan_linked_assets(Operator):
return {'FINISHED'}
+class DYNAMICLINK_OT_fmt_style_find(Operator):
+ """Find missing libraries in search folders using FMT-style approach"""
+ bl_idname = "dynamiclink.fmt_style_find"
+ bl_label = "FMT-Style Find"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def execute(self, context):
+ prefs = context.preferences.addons.get(__package__)
+ if not prefs or not prefs.preferences.search_paths:
+ self.report({'ERROR'}, "No search paths configured. Add search paths in addon preferences.")
+ return {'CANCELLED'}
+
+ # Get missing libraries
+ missing_libs = [lib for lib in context.scene.dynamic_link_manager.linked_libraries if lib.is_missing]
+ if not missing_libs:
+ self.report({'INFO'}, "No missing libraries to find")
+ return {'FINISHED'}
+
+ self.report({'INFO'}, f"FMT-style search for {len(missing_libs)} missing libraries...")
+
+ # FMT-style directory scanning (exact copy of FMT logic)
+ files_dir_list = []
+ try:
+ for search_path in prefs.preferences.search_paths:
+ if search_path.path:
+ for dirpath, dirnames, filenames in os.walk(bpy.path.abspath(search_path.path)):
+ files_dir_list.append([dirpath, filenames])
+ except FileNotFoundError:
+ self.report({'ERROR'}, f"Error - Bad file path in search paths")
+ return {'CANCELLED'}
+
+ # FMT-style library finding
+ found_count = 0
+
+ for lib_item in missing_libs:
+ lib_filename = os.path.basename(lib_item.filepath)
+
+ for dir_info in files_dir_list:
+ dirpath, filenames = dir_info
+
+ # Exact filename match
+ if lib_filename in filenames:
+ new_path = os.path.join(dirpath, lib_filename)
+ self.report({'INFO'}, f"Found {lib_filename} at: {new_path}")
+ found_count += 1
+ break
+
+ # Extension replacement (FMT-style)
+ elif context.scene.dynamic_link_manager.search_different_extensions:
+ base_name = os.path.splitext(lib_filename)[0]
+ for filename in filenames:
+ if base_name == os.path.splitext(filename)[0]:
+ new_path = os.path.join(dirpath, filename)
+ self.report({'INFO'}, f"Found {lib_filename} (as {filename}) at: {new_path}")
+ found_count += 1
+ break
+
+ # FMT-style reporting
+ if found_count > 0:
+ self.report({'INFO'}, f"FMT-style search complete: Found {found_count} libraries")
+ else:
+ self.report({'WARNING'}, "FMT-style search: No libraries found in search paths")
+
+ return {'FINISHED'}
+
def register():
bpy.utils.register_class(DYNAMICLINK_OT_replace_linked_asset)
bpy.utils.register_class(DYNAMICLINK_OT_scan_linked_assets)
@@ -305,7 +370,7 @@ class DYNAMICLINK_OT_attempt_relink(Operator):
self.report({'ERROR'}, f"Error - Bad file path in search paths")
return {'CANCELLED'}
- # Try to find and relink each missing library
+ # Try to find and relink each missing library (FMT-style)
relinked_count = 0
indirect_errors = []
@@ -334,6 +399,28 @@ class DYNAMICLINK_OT_attempt_relink(Operator):
print(f"Indirect link detected for: {lib_item.filepath}")
else:
print(f"Error relinking {lib_item.filepath}: {e}")
+
+ # FMT-style extension replacement
+ if not found and context.scene.dynamic_link_manager.search_different_extensions:
+ base_name = os.path.splitext(lib_filename)[0]
+ for filename in filenames:
+ if base_name == os.path.splitext(filename)[0]:
+ new_path = os.path.join(dirpath, filename)
+ try:
+ bpy.ops.file.find_missing_files()
+ found = True
+ relinked_count += 1
+ break
+ except Exception as e:
+ error_msg = str(e)
+ if "unable to relocate indirectly linked library" in error_msg:
+ indirect_errors.append(lib_item.filepath)
+ print(f"Indirect link detected for: {lib_item.filepath}")
+ else:
+ print(f"Error relinking {lib_item.filepath}: {e}")
+
+ if found:
+ break
if not found:
print(f"Could not find {lib_filename} in search paths")
@@ -370,7 +457,7 @@ class DYNAMICLINK_OT_find_in_folders(Operator):
self.report({'INFO'}, f"Searching for {len(missing_libs)} missing libraries...")
- # Scan search paths for missing libraries (FMT-inspired approach)
+ # FMT-style directory scanning
files_dir_list = []
try:
for search_path in prefs.preferences.search_paths:
@@ -381,11 +468,12 @@ class DYNAMICLINK_OT_find_in_folders(Operator):
self.report({'ERROR'}, f"Error - Bad file path in search paths")
return {'CANCELLED'}
- # Try to find each missing library
+ # Try to find each missing library (FMT-style)
found_count = 0
for lib_item in missing_libs:
lib_filename = os.path.basename(lib_item.filepath)
+ found = False
# Search through all directories
for dir_info in files_dir_list:
@@ -396,7 +484,25 @@ class DYNAMICLINK_OT_find_in_folders(Operator):
new_path = os.path.join(dirpath, lib_filename)
self.report({'INFO'}, f"Found {lib_filename} at: {new_path}")
found_count += 1
+ found = True
break
+
+ # FMT-style extension replacement
+ if context.scene.dynamic_link_manager.search_different_extensions:
+ base_name = os.path.splitext(lib_filename)[0]
+ for filename in filenames:
+ if base_name == os.path.splitext(filename)[0]:
+ new_path = os.path.join(dirpath, filename)
+ self.report({'INFO'}, f"Found {lib_filename} (as {filename}) at: {new_path}")
+ found_count += 1
+ found = True
+ break
+
+ if found:
+ break
+
+ if not found:
+ self.report({'WARNING'}, f"Could not find {lib_filename} in search paths")
if found_count > 0:
self.report({'INFO'}, f"Found {found_count} libraries in search paths")
@@ -413,8 +519,10 @@ def register():
bpy.utils.register_class(DYNAMICLINK_OT_remove_search_path)
bpy.utils.register_class(DYNAMICLINK_OT_attempt_relink)
bpy.utils.register_class(DYNAMICLINK_OT_find_in_folders)
+ bpy.utils.register_class(DYNAMICLINK_OT_fmt_style_find)
def unregister():
+ bpy.utils.unregister_class(DYNAMICLINK_OT_fmt_style_find)
bpy.utils.unregister_class(DYNAMICLINK_OT_find_in_folders)
bpy.utils.unregister_class(DYNAMICLINK_OT_attempt_relink)
bpy.utils.unregister_class(DYNAMICLINK_OT_remove_search_path)
diff --git a/ui.py b/ui.py
index 385d92a..9bf530b 100644
--- a/ui.py
+++ b/ui.py
@@ -238,6 +238,37 @@ class DYNAMICLINK_PT_main_panel(Panel):
row = box.row()
row.operator("dynamiclink.attempt_relink", text="Attempt to relink found libraries", icon='FILE_REFRESH')
+ # FMT-Style Search Paths Management (Direct Copy)
+ box = layout.box()
+ box.label(text="FMT-Style Search Paths")
+
+ # Get preferences
+ prefs = context.preferences.addons.get(__package__)
+ if prefs:
+ # Extension replacement option (FMT-style)
+ if len(prefs.preferences.search_paths) > 0:
+ row = box.row()
+ row.prop(props, "search_different_extensions", text="Search for libraries with different extensions")
+
+ # Search paths list (FMT-style)
+ if prefs.preferences.search_paths:
+ for i, path_item in enumerate(prefs.preferences.search_paths):
+ row = box.row()
+ row.prop(path_item, "path", text=f"Search path {i+1}")
+ row.operator("dynamiclink.remove_search_path", text="", icon='REMOVE').index = i
+
+ # Add/remove buttons (FMT-style)
+ row = box.row()
+ row.operator("dynamiclink.add_search_path", text="Add Search Path", icon='ADD')
+ row.operator("dynamiclink.remove_search_path", text="Remove Selected", icon='REMOVE')
+
+ # Main action button (FMT-style)
+ if props.linked_assets_count > 0:
+ missing_count = sum(1 for lib in props.linked_libraries if lib.is_missing)
+ if missing_count > 0:
+ row = box.row()
+ row.operator("dynamiclink.fmt_style_find", text="Find libraries in these folders", icon='VIEWZOOM')
+
# Settings section (FMT-style)
box = layout.box()
box.label(text="Settings")