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 40b9ce6..b34944e 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
@@ -1073,3 +1073,118 @@ The extension is ready to be installed in Blender 4.5.0+ and should appear in th
---
+_**User**_
+
+alright, let's simplify. I restarted by using this blender development vscode addon. We should be able to build off this structure.
+
+---
+
+_**Assistant**_
+
+Great idea! Using the Blender development VSCode addon will give us a much better development experience. Let me check what structure we have now and see how we can build on it.
+
+
+---
+
+
+ Listed current directory • **6** results
+
+| Name |
+|-------|
+| 📁 `.git` |
+| 📄 `.gitignore` |
+| 📄 `blender_manifest.toml` |
+| 📄 `__init__.py` |
+| 📄 `.cursorindexingignore` |
+| 📁 `.specstory` |
+
+
+
+---
+
+Perfect! I can see you have the basic structure with the VSCode addon. Let me check what's in the `__init__.py` to see what the addon generated:
+
+
+---
+
+Read file: __init__.py
+
+---
+
+Great! The VSCode addon has given us a clean starting point. Now let me check the manifest to see what it generated:
+
+
+---
+
+Read file: blender_manifest.toml
+
+---
+
+Excellent! The VSCode addon has given us a much cleaner and more modern structure. Now let me update the manifest with your specific details and then build the core functionality. Let me start by updating the manifest:
+
+
+---
+
+Edit file: blender_manifest.toml
+
+undefined
+
+**Chunk 1**
+Lines added: 5, lines removed: 5
+
+```diff
+@@ -3,9 +3,9 @@
+ # Example of manifest file for a Blender extension
+ # Change the values according to your extension
+- id = "dynamic-link-manager"
+- version = "1.0.0"
+- name = "Dynamic-link-manager"
+- tagline = "This is another extension"
+- maintainer = "RaincloudTheDragon"
++ id = "dynamiclinkmanager"
++ version = "0.5.1"
++ name = "Dynamic Link Manager"
++ tagline = "Replace linked assets and characters with ease"
++ maintainer = "Nathan Lindsay"
+ # Supported types: "add-on", "theme"
+ type = "add-on"
+```
+
+
+
+---
+
+Edit file: blender_manifest.toml
+
+undefined
+
+**Chunk 1**
+Lines added: 1, lines removed: 1
+
+```diff
+@@ -16,5 +16,5 @@
+ # Optional list defined by Blender and server, see:
+ # https://docs.blender.org/manual/en/dev/advanced/extensions/tags.html
+- tags = ["Animation", "Sequencer"]
++ tags = ["3D View", "Import-Export"]
+
+ blender_version_min = "4.2.0"
+```
+
+
+
+---
+
+Now let me update the `__init__.py` with the proper addon structure:
+
+
+---
+
+
+
+---
+
+
+
+---
+
diff --git a/__init__.py b/__init__.py
index 928f378..3b8a0ba 100644
--- a/__init__.py
+++ b/__init__.py
@@ -11,7 +11,33 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-def register(): ...
+bl_info = {
+ "name": "Dynamic Link Manager",
+ "author": "Nathan Lindsay",
+ "version": (0, 0, 1),
+ "blender": (4, 5, 0),
+ "location": "View3D > Sidebar > Dynamic Link Manager",
+ "description": "Replace linked assets and characters with ease",
+ "warning": "",
+ "doc_url": "",
+ "category": "Import-Export",
+}
+import bpy
+from bpy.props import StringProperty, BoolProperty, EnumProperty
+from bpy.types import Panel, Operator, PropertyGroup
-def unregister(): ...
+# Import local modules
+from . import operators
+from . import ui
+
+def register():
+ operators.register()
+ ui.register()
+
+def unregister():
+ ui.unregister()
+ operators.unregister()
+
+if __name__ == "__main__":
+ register()
diff --git a/blender_manifest.toml b/blender_manifest.toml
index b320539..5182f7f 100644
--- a/blender_manifest.toml
+++ b/blender_manifest.toml
@@ -2,11 +2,11 @@ schema_version = "1.0.0"
# Example of manifest file for a Blender extension
# Change the values according to your extension
-id = "dynamic-link-manager"
-version = "1.0.0"
-name = "Dynamic-link-manager"
-tagline = "This is another extension"
-maintainer = "RaincloudTheDragon"
+id = "dynamiclinkmanager"
+version = "0.0.1"
+name = "Dynamic Link Manager"
+tagline = "Replace linked assets and characters with ease"
+maintainer = "Nathan Lindsay"
# Supported types: "add-on", "theme"
type = "add-on"
@@ -15,7 +15,7 @@ type = "add-on"
# Optional list defined by Blender and server, see:
# https://docs.blender.org/manual/en/dev/advanced/extensions/tags.html
-tags = ["Animation", "Sequencer"]
+tags = ["3D View", "Import-Export"]
blender_version_min = "4.2.0"
# # Optional: Blender version that the extension does not support, earlier versions are supported.
diff --git a/operators.py b/operators.py
new file mode 100644
index 0000000..06d560a
--- /dev/null
+++ b/operators.py
@@ -0,0 +1,78 @@
+import bpy
+import os
+from bpy.types import Operator
+from bpy.props import StringProperty, BoolProperty, EnumProperty
+
+class DYNAMICLINK_OT_replace_linked_asset(Operator):
+ """Replace a linked asset with a new file"""
+ bl_idname = "dynamiclink.replace_linked_asset"
+ bl_label = "Replace Linked Asset"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ filepath: StringProperty(
+ name="File Path",
+ description="Path to the new asset file",
+ subtype='FILE_PATH'
+ )
+
+ def execute(self, context):
+ obj = context.active_object
+ if not obj:
+ self.report({'ERROR'}, "No object selected")
+ return {'CANCELLED'}
+
+ if not obj.data or not obj.data.library:
+ self.report({'ERROR'}, "Selected object is not linked")
+ return {'CANCELLED'}
+
+ try:
+ # Implementation will go here
+ self.report({'INFO'}, f"Replaced linked asset: {obj.name}")
+ return {'FINISHED'}
+ except Exception as e:
+ self.report({'ERROR'}, f"Failed to replace asset: {str(e)}")
+ return {'CANCELLED'}
+
+ def invoke(self, context, event):
+ context.window_manager.fileselect_add(self)
+ return {'RUNNING_MODAL'}
+
+class DYNAMICLINK_OT_scan_linked_assets(Operator):
+ """Scan scene for all linked assets"""
+ bl_idname = "dynamiclink.scan_linked_assets"
+ bl_label = "Scan Linked Assets"
+
+ def execute(self, context):
+ linked_assets = []
+
+ # Scan objects
+ for obj in bpy.data.objects:
+ if obj.library:
+ linked_assets.append({
+ 'type': 'OBJECT',
+ 'name': obj.name,
+ 'library': obj.library.filepath
+ })
+
+ # Scan meshes
+ for mesh in bpy.data.meshes:
+ if mesh.library:
+ linked_assets.append({
+ 'type': 'MESH',
+ 'name': mesh.name,
+ 'library': mesh.library.filepath
+ })
+
+ # Store results in scene properties
+ context.scene.dynamic_link_manager.linked_assets_count = len(linked_assets)
+
+ self.report({'INFO'}, f"Found {len(linked_assets)} linked assets")
+ return {'FINISHED'}
+
+def register():
+ bpy.utils.register_class(DYNAMICLINK_OT_replace_linked_asset)
+ bpy.utils.register_class(DYNAMICLINK_OT_scan_linked_assets)
+
+def unregister():
+ bpy.utils.unregister_class(DYNAMICLINK_OT_scan_linked_assets)
+ bpy.utils.unregister_class(DYNAMICLINK_OT_replace_linked_asset)
diff --git a/ui.py b/ui.py
new file mode 100644
index 0000000..bf7d166
--- /dev/null
+++ b/ui.py
@@ -0,0 +1,75 @@
+import bpy
+from bpy.types import Panel, PropertyGroup
+from bpy.props import IntProperty, StringProperty
+
+class DynamicLinkManagerProperties(PropertyGroup):
+ linked_assets_count: IntProperty(
+ name="Linked Assets Count",
+ description="Number of linked assets found in scene",
+ default=0
+ )
+
+ selected_asset_path: StringProperty(
+ name="Selected Asset Path",
+ description="Path to the currently selected asset",
+ default=""
+ )
+
+class DYNAMICLINK_PT_main_panel(Panel):
+ bl_space_type = 'VIEW_3D'
+ bl_region_type = 'UI'
+ bl_category = 'Dynamic Link Manager'
+ bl_label = "Asset Replacement"
+
+ def draw(self, context):
+ layout = self.layout
+ props = context.scene.dynamic_link_manager
+
+ # Scan section
+ box = layout.box()
+ box.label(text="Scene Analysis")
+ row = box.row()
+ row.operator("dynamiclink.scan_linked_assets", text="Scan Linked Assets")
+ row.label(text=f"Found: {props.linked_assets_count}")
+
+ # Asset replacement section
+ box = layout.box()
+ box.label(text="Asset Replacement")
+
+ obj = context.active_object
+ if obj:
+ box.label(text=f"Selected: {obj.name}")
+ if obj.library:
+ box.label(text=f"Linked from: {obj.library.filepath}")
+ row = box.row()
+ row.operator("dynamiclink.replace_linked_asset", text="Replace Asset")
+ else:
+ box.label(text="Not a linked asset")
+ else:
+ box.label(text="No object selected")
+
+ # Batch operations section
+ box = layout.box()
+ box.label(text="Batch Operations")
+ row = box.row()
+ row.operator("dynamiclink.scan_linked_assets", text="Refresh Scan")
+
+ # Settings section
+ box = layout.box()
+ box.label(text="Settings")
+ row = box.row()
+ row.prop(props, "selected_asset_path", text="Asset Path")
+
+def register():
+ bpy.utils.register_class(DynamicLinkManagerProperties)
+ bpy.utils.register_class(DYNAMICLINK_PT_main_panel)
+
+ # Register properties to scene
+ bpy.types.Scene.dynamic_link_manager = bpy.props.PointerProperty(type=DynamicLinkManagerProperties)
+
+def unregister():
+ # Unregister properties from scene
+ del bpy.types.Scene.dynamic_link_manager
+
+ bpy.utils.unregister_class(DYNAMICLINK_PT_main_panel)
+ bpy.utils.unregister_class(DynamicLinkManagerProperties)