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)