FBX IO: Vertex position access with attributes
[blender-addons.git] / object_edit_linked.py
blob479ebe84bb4cc0af7e67102b38523239bf3941ea
1 # SPDX-FileCopyrightText: 2013-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 bl_info = {
6 "name": "Edit Linked Library",
7 "author": "Jason van Gumster (Fweeb), Bassam Kurdali, Pablo Vazquez, Rainer Trummer",
8 "version": (0, 9, 2),
9 "blender": (2, 80, 0),
10 "location": "File > External Data / View3D > Sidebar > Item Tab / Node Editor > Sidebar > Node Tab",
11 "description": "Allows editing of objects, collections, and node groups linked from a .blend library.",
12 "doc_url": "{BLENDER_MANUAL_URL}/addons/object/edit_linked_library.html",
13 "category": "Object",
16 import bpy
17 import logging
18 import os
20 from bpy.app.handlers import persistent
22 logger = logging.getLogger('object_edit_linked')
24 settings = {
25 "original_file": "",
26 "linked_file": "",
27 "linked_objects": [],
28 "linked_nodes": []
32 @persistent
33 def linked_file_check(context: bpy.context):
34 if settings["linked_file"] != "":
35 if os.path.samefile(settings["linked_file"], bpy.data.filepath):
36 logger.info("Editing a linked library.")
37 bpy.ops.object.select_all(action='DESELECT')
38 for ob_name in settings["linked_objects"]:
39 bpy.data.objects[ob_name].select_set(True) # XXX Assumes selected object is in the active scene
40 if len(settings["linked_objects"]) == 1:
41 context.view_layer.objects.active = bpy.data.objects[settings["linked_objects"][0]]
42 else:
43 # For some reason, the linked editing session ended
44 # (failed to find a file or opened a different file
45 # before returning to the originating .blend)
46 settings["original_file"] = ""
47 settings["linked_file"] = ""
50 class OBJECT_OT_EditLinked(bpy.types.Operator):
51 """Edit Linked Library"""
52 bl_idname = "object.edit_linked"
53 bl_label = "Edit Linked Library"
55 use_autosave: bpy.props.BoolProperty(
56 name="Autosave",
57 description="Save the current file before opening the linked library",
58 default=True)
59 use_instance: bpy.props.BoolProperty(
60 name="New Blender Instance",
61 description="Open in a new Blender instance",
62 default=False)
64 @classmethod
65 def poll(cls, context: bpy.context):
66 return settings["original_file"] == "" and context.active_object is not None and (
67 (context.active_object.instance_collection and
68 context.active_object.instance_collection.library is not None) or
69 context.active_object.library is not None or
70 (context.active_object.override_library and
71 context.active_object.override_library.reference.library is not None))
73 def execute(self, context: bpy.context):
74 target = context.active_object
76 if target.instance_collection and target.instance_collection.library:
77 targetpath = target.instance_collection.library.filepath
78 settings["linked_objects"].extend({ob.name for ob in target.instance_collection.objects})
79 elif target.library:
80 targetpath = target.library.filepath
81 settings["linked_objects"].append(target.name)
82 elif target.override_library:
83 target = target.override_library.reference
84 targetpath = target.library.filepath
85 settings["linked_objects"].append(target.name)
87 if targetpath:
88 logger.debug(target.name + " is linked to " + targetpath)
90 if self.use_autosave:
91 if not bpy.data.filepath:
92 # File is not saved on disk, better to abort!
93 self.report({'ERROR'}, "Current file does not exist on disk, we cannot autosave it, aborting")
94 return {'CANCELLED'}
95 bpy.ops.wm.save_mainfile()
97 settings["original_file"] = bpy.data.filepath
98 # Using both bpy and os abspath functions because Windows doesn't like relative routes as part of an absolute path
99 settings["linked_file"] = os.path.abspath(bpy.path.abspath(targetpath))
101 if self.use_instance:
102 import subprocess
103 try:
104 subprocess.Popen([bpy.app.binary_path, settings["linked_file"]])
105 except:
106 logger.error("Error on the new Blender instance")
107 import traceback
108 logger.error(traceback.print_exc())
109 else:
110 bpy.ops.wm.open_mainfile(filepath=settings["linked_file"])
112 logger.info("Opened linked file!")
113 else:
114 self.report({'WARNING'}, target.name + " is not linked")
115 logger.warning(target.name + " is not linked")
117 return {'FINISHED'}
120 class NODE_OT_EditLinked(bpy.types.Operator):
121 """Edit Linked Library"""
122 bl_idname = "node.edit_linked"
123 bl_label = "Edit Linked Library"
125 use_autosave: bpy.props.BoolProperty(
126 name="Autosave",
127 description="Save the current file before opening the linked library",
128 default=True)
129 use_instance: bpy.props.BoolProperty(
130 name="New Blender Instance",
131 description="Open in a new Blender instance",
132 default=False)
134 @classmethod
135 def poll(cls, context: bpy.context):
136 return settings["original_file"] == "" and context.active_node is not None and (
137 (context.active_node.type == 'GROUP' and
138 hasattr(context.active_node.node_tree, "library") and
139 context.active_node.node_tree.library is not None) or
140 (hasattr(context.active_node, "monad") and
141 context.active_node.monad.library is not None))
143 def execute(self, context: bpy.context):
144 target = context.active_node
145 if (target.type == "GROUP"):
146 target = target.node_tree
147 else:
148 target = target.monad
150 targetpath = target.library.filepath
151 settings["linked_nodes"].append(target.name)
153 if targetpath:
154 logger.debug(target.name + " is linked to " + targetpath)
156 if self.use_autosave:
157 if not bpy.data.filepath:
158 # File is not saved on disk, better to abort!
159 self.report({'ERROR'}, "Current file does not exist on disk, we cannot autosave it, aborting")
160 return {'CANCELLED'}
161 bpy.ops.wm.save_mainfile()
163 settings["original_file"] = bpy.data.filepath
164 # Using both bpy and os abspath functions because Windows doesn't like relative routes as part of an absolute path
165 settings["linked_file"] = os.path.abspath(bpy.path.abspath(targetpath))
167 if self.use_instance:
168 import subprocess
169 try:
170 subprocess.Popen([bpy.app.binary_path, settings["linked_file"]])
171 except:
172 logger.error("Error on the new Blender instance")
173 import traceback
174 logger.error(traceback.print_exc())
175 else:
176 bpy.ops.wm.open_mainfile(filepath=settings["linked_file"])
178 logger.info("Opened linked file!")
179 else:
180 self.report({'WARNING'}, target.name + " is not linked")
181 logger.warning(target.name + " is not linked")
183 return {'FINISHED'}
186 class WM_OT_ReturnToOriginal(bpy.types.Operator):
187 """Load the original file"""
188 bl_idname = "wm.return_to_original"
189 bl_label = "Return to Original File"
191 use_autosave: bpy.props.BoolProperty(
192 name="Autosave",
193 description="Save the current file before opening original file",
194 default=True)
196 @classmethod
197 def poll(cls, context: bpy.context):
198 return (settings["original_file"] != "")
200 def execute(self, context: bpy.context):
201 if self.use_autosave:
202 bpy.ops.wm.save_mainfile()
204 bpy.ops.wm.open_mainfile(filepath=settings["original_file"])
206 settings["original_file"] = ""
207 settings["linked_objects"] = []
208 logger.info("Back to the original!")
209 return {'FINISHED'}
212 class VIEW3D_PT_PanelLinkedEdit(bpy.types.Panel):
213 bl_label = "Edit Linked Library"
214 bl_space_type = "VIEW_3D"
215 bl_region_type = 'UI'
216 bl_category = "Item"
217 bl_context = 'objectmode'
218 bl_options = {'DEFAULT_CLOSED'}
220 @classmethod
221 def poll(cls, context: bpy.context):
222 return (context.active_object is not None) or (settings["original_file"] != "")
224 def draw_common(self, scene, layout, props):
225 if props is not None:
226 props.use_autosave = scene.use_autosave
227 props.use_instance = scene.use_instance
229 layout.prop(scene, "use_autosave")
230 layout.prop(scene, "use_instance")
232 def draw(self, context: bpy.context):
233 scene = context.scene
234 layout = self.layout
235 layout.use_property_split = False
236 layout.use_property_decorate = False
237 icon = "OUTLINER_DATA_" + context.active_object.type.replace("LIGHT_PROBE", "LIGHTPROBE")
239 target = None
241 target = context.active_object.instance_collection
243 if settings["original_file"] == "" and (
244 (target and
245 target.library is not None) or
246 context.active_object.library is not None or
247 (context.active_object.override_library is not None and
248 context.active_object.override_library.reference is not None)):
250 if (target is not None):
251 props = layout.operator("object.edit_linked", icon="LINK_BLEND",
252 text="Edit Library: %s" % target.name)
253 elif (context.active_object.library):
254 props = layout.operator("object.edit_linked", icon="LINK_BLEND",
255 text="Edit Library: %s" % context.active_object.name)
256 else:
257 props = layout.operator("object.edit_linked", icon="LINK_BLEND",
258 text="Edit Override Library: %s" % context.active_object.override_library.reference.name)
260 self.draw_common(scene, layout, props)
262 if (target is not None):
263 layout.label(text="Path: %s" %
264 target.library.filepath)
265 elif (context.active_object.library):
266 layout.label(text="Path: %s" %
267 context.active_object.library.filepath)
268 else:
269 layout.label(text="Path: %s" %
270 context.active_object.override_library.reference.library.filepath)
272 elif settings["original_file"] != "":
274 if scene.use_instance:
275 layout.operator("wm.return_to_original",
276 text="Reload Current File",
277 icon="FILE_REFRESH").use_autosave = False
279 layout.separator()
281 # XXX - This is for nested linked assets... but it only works
282 # when launching a new Blender instance. Nested links don't
283 # currently work when using a single instance of Blender.
284 if context.active_object.instance_collection is not None:
285 props = layout.operator("object.edit_linked",
286 text="Edit Library: %s" % context.active_object.instance_collection.name,
287 icon="LINK_BLEND")
288 else:
289 props = None
291 self.draw_common(scene, layout, props)
293 if context.active_object.instance_collection is not None:
294 layout.label(text="Path: %s" %
295 context.active_object.instance_collection.library.filepath)
297 else:
298 props = layout.operator("wm.return_to_original", icon="LOOP_BACK")
299 props.use_autosave = scene.use_autosave
301 layout.prop(scene, "use_autosave")
303 else:
304 layout.label(text="%s is not linked" % context.active_object.name,
305 icon=icon)
308 class NODE_PT_PanelLinkedEdit(bpy.types.Panel):
309 bl_label = "Edit Linked Library"
310 bl_space_type = 'NODE_EDITOR'
311 bl_region_type = 'UI'
312 if bpy.app.version >= (2, 93, 0):
313 bl_category = "Node"
314 else:
315 bl_category = "Item"
316 bl_options = {'DEFAULT_CLOSED'}
318 @classmethod
319 def poll(cls, context):
320 return context.active_node is not None
322 def draw_common(self, scene, layout, props):
323 if props is not None:
324 props.use_autosave = scene.use_autosave
325 props.use_instance = scene.use_instance
327 layout.prop(scene, "use_autosave")
328 layout.prop(scene, "use_instance")
330 def draw(self, context):
331 scene = context.scene
332 layout = self.layout
333 layout.use_property_split = False
334 layout.use_property_decorate = False
335 icon = 'NODETREE'
337 target = context.active_node
339 if settings["original_file"] == "" and (
340 (target.type == 'GROUP' and hasattr(target.node_tree, "library") and
341 target.node_tree.library is not None) or
342 (hasattr(target, "monad") and target.monad.library is not None)):
344 if (target.type == "GROUP"):
345 props = layout.operator("node.edit_linked", icon="LINK_BLEND",
346 text="Edit Library: %s" % target.name)
347 else:
348 props = layout.operator("node.edit_linked", icon="LINK_BLEND",
349 text="Edit Library: %s" % target.monad.name)
351 self.draw_common(scene, layout, props)
353 if (target.type == "GROUP"):
354 layout.label(text="Path: %s" % target.node_tree.library.filepath)
355 else:
356 layout.label(text="Path: %s" % target.monad.library.filepath)
358 elif settings["original_file"] != "":
360 if scene.use_instance:
361 layout.operator("wm.return_to_original",
362 text="Reload Current File",
363 icon="FILE_REFRESH").use_autosave = False
365 layout.separator()
367 props = None
369 self.draw_common(scene, layout, props)
371 #layout.label(text="Path: %s" %
372 # context.active_object.instance_collection.library.filepath)
374 else:
375 props = layout.operator("wm.return_to_original", icon="LOOP_BACK")
376 props.use_autosave = scene.use_autosave
378 layout.prop(scene, "use_autosave")
380 else:
381 layout.label(text="%s is not linked" % target.name, icon=icon)
384 class TOPBAR_MT_edit_linked_submenu(bpy.types.Menu):
385 bl_label = 'Edit Linked Library'
387 def draw(self, context):
388 self.layout.separator()
389 self.layout.operator(OBJECT_OT_EditLinked.bl_idname)
390 self.layout.operator(WM_OT_ReturnToOriginal.bl_idname)
393 addon_keymaps = []
394 classes = (
395 OBJECT_OT_EditLinked,
396 NODE_OT_EditLinked,
397 WM_OT_ReturnToOriginal,
398 VIEW3D_PT_PanelLinkedEdit,
399 NODE_PT_PanelLinkedEdit,
400 TOPBAR_MT_edit_linked_submenu
404 def register():
405 bpy.app.handlers.load_post.append(linked_file_check)
407 for c in classes:
408 bpy.utils.register_class(c)
410 bpy.types.Scene.use_autosave = bpy.props.BoolProperty(
411 name="Autosave",
412 description="Save the current file before opening a linked file",
413 default=True)
415 bpy.types.Scene.use_instance = bpy.props.BoolProperty(
416 name="New Blender Instance",
417 description="Open in a new Blender instance",
418 default=False)
420 # add the function to the file menu
421 bpy.types.TOPBAR_MT_file_external_data.append(TOPBAR_MT_edit_linked_submenu.draw)
426 def unregister():
428 bpy.app.handlers.load_post.remove(linked_file_check)
429 bpy.types.TOPBAR_MT_file_external_data.remove(TOPBAR_MT_edit_linked_submenu)
431 del bpy.types.Scene.use_autosave
432 del bpy.types.Scene.use_instance
435 for c in reversed(classes):
436 bpy.utils.unregister_class(c)
439 if __name__ == "__main__":
440 register()