From 08d73b3f6f74358f469ee90fe3fef8dcde700010 Mon Sep 17 00:00:00 2001 From: Andy Goralczyk Date: Fri, 12 Jun 2020 10:24:08 +0200 Subject: [PATCH] Initial conversion to work with 2.8. collections are used instead of the old group system. As an additional asset component type, Non-instanced collections have been added back (this used to be supported by the pre-blender institute version developed by Bassam Kurdali afaik. --- README.md | 4 + __init__.py | 942 ++++++++++++++++++++++++++++++++++++++++++++++++++++ linking.py | 184 ++++++++++ 3 files changed, 1130 insertions(+) create mode 100644 __init__.py create mode 100644 linking.py diff --git a/README.md b/README.md index f1b255c..4482893 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,6 @@ # PowerLib This is a Blender 2.8+ port of the Powerlib Addon developed for Agent 327: Operation Barbershop. +<<<<<<< HEAD +======= + +>>>>>>> Initial conversion to work with 2.8. collections are used instead of the old group system. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..52f71d8 --- /dev/null +++ b/__init__.py @@ -0,0 +1,942 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ***** END GPL LICENCE BLOCK ***** + +# + +from bpy.props import ( + BoolProperty, + IntProperty, + StringProperty, + EnumProperty, + CollectionProperty, + PointerProperty, +) +from bpy.types import ( + Operator, + Menu, + Panel, + UIList, + PropertyGroup, +) +from bpy.app.handlers import persistent +import bpy +import json +import os +bl_info = { + "name": "Powerlib", + "author": "InĂªs Almeida, Francesco Siddi, Olivier Amrein, Dalai Felinto", + "version": (2, 0, 0), + "blender": (2, 90, 0), + "location": "View3D > Properties (N)", + "description": "Asset management", + "warning": "", + "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/" + "Scripts/Workflow/-todo-create-new-documentation-page!", + "category": "Workflow", +} + + +VERBOSE = False # enable this for debugging + + +def debug_print(*args): + """Print debug messages""" + if VERBOSE: + print(*args) + + +# Data Structure ############################################################## + +# A powerlib library is structured as a series of collections of assets. +# An asset has a set of components which are organized by type. +# eg. the library defined in XXX has the Collections "Characters" and "Props" +# the collection "Characters" contains the assets "Boris" and "Agent327" +# an asset has one or more components of type "instance_collections" +# which consist of a path to the blend file and the name of the collection to instance + +runtime_vars = {} + + +class ReadState: + NotLoaded, NoFile, FilePathInvalid, FileContentInvalid, EmptyLib, AllGood = range( + 6) + + +runtime_vars["read_state"] = ReadState.NotLoaded + + +class SaveState: + HasUnsavedChanges, AllSaved = range(2) + + +runtime_vars["save_state"] = SaveState.AllSaved + + +enum_component_type = EnumProperty( + items=( + ('INSTANCE_COLLECTIONS', "Instance Collections", "", 'EMPTY_DATA', 0), + ('NON_INSTANCE_COLLECTIONS', "Non instance Collections", "", 'GROUP', 1), + ('COLLECTION_REFERENCE_OBJECTS', + "Collection Reference Objects", "", 'OBJECT_DATA', 2), + ), + default='INSTANCE_COLLECTIONS', + name="Component Type", + description="Type of an asset component", +) + + +def enum_item_name_icon(enum, value): + """Return the item name and icon of an enum + """ + prop, settings = enum + + for item in settings['items']: + if item[0] == value: + return item[1], item[3] + return "Error", 'ERROR' + + +class ComponentItem(PropertyGroup): + name: StringProperty() + + +class Component(PropertyGroup): + def update_filepath_rel(self, context): + """Updates the filepath property after we picked a new value for + filepath_rel via a file browser. + """ + # ~ self.collection = None + self.collections.clear() + + if self.filepath_rel == '': + return + # TODO: ensure path is valid + # Make path relative to the library + from . import linking + import importlib + importlib.reload(linking) + + fp_rel_to_lib = linking.relative_path_to_lib(self.filepath_rel) + debug_print(f'Updating library link to {fp_rel_to_lib}') + self.filepath = fp_rel_to_lib + + cache_key = self.filepath + if self.filepath_rel == '//' + os.path.basename(bpy.data.filepath): + for col in bpy.data.collections: + if col.library: + continue + self.collections.add().name = col.name + else: + filepath = bpy.path.relpath(self.absolute_filepath) + with bpy.data.libraries.load(filepath) as (data_from, data_to): + for colname in data_from.collections: + self.collections.add().name = colname + + id: StringProperty( + name="Name", + description="Name for this component, eg. the name of a collection", + ) + + collections: CollectionProperty(type=ComponentItem) + + filepath: StringProperty( + name="File path", + description="Path to the blend file which holds this data relative to the library", + subtype='FILE_PATH', + ) + + filepath_rel: StringProperty( + name="Relative file path", + description="Path to the blend file which holds this data relative from the current file", + subtype='FILE_PATH', + update=update_filepath_rel, + ) + + @property + def absolute_filepath(self): + library_path = os.path.dirname( + bpy.path.abspath(bpy.context.scene['lib_path'])) + abspath = os.path.join(library_path, self.filepath) + normpath = os.path.normpath(abspath) + if os.path.isfile(normpath): + return normpath + else: + # raise IOError('File {} not found'.format(normpath)) + debug_print(f'IOError: File {normpath} not found') + + +class ComponentsList(PropertyGroup): + """A set of components of a certain type that build an asset + example types: (collections, collection_reference_objects, scripts). + """ + component_type: enum_component_type + + components: CollectionProperty( + name="Components", + description="List of components of this type", + type=Component, + ) + active_component: IntProperty( + name="Selected Component", + description="Currently selected component of this type", + ) + + @staticmethod + def getComponentType(name): + lookup = { + 'instance_collections': 'INSTANCE_COLLECTIONS', + 'non_instance_collections': 'NON_INSTANCE_COLLECTIONS', + 'collection_reference_objects': 'COLLECTION_REFERENCE_OBJECTS', + } + value = lookup.get(name) + if value is None: + raise Exception(f"Component type not supported: {name}") + + return value + + +class AssetItem(PropertyGroup): + components_by_type: CollectionProperty( + name="Components by Type", + type=ComponentsList, + ) + + +class AssetCollection(PropertyGroup): + active_asset: IntProperty( + name="Selected Asset", + description="Currently selected asset", + ) + assets: CollectionProperty( + name="Assets", + description="List of assets in this collection", + type=AssetItem, + ) + + +class PowerProperties(PropertyGroup): + is_edit_mode: BoolProperty( + name="Is in Edit Mode", + description="Toggle for Edit/Selection mode", + default=False, + ) + + collections: CollectionProperty( + name="PowerLib Collections", + description="List of Asset Collections in the active library", + type=AssetCollection, + ) + + active_col: StringProperty( + name="Active Category", + description="Currently selected Asset Category", + ) + + +# Operators ################################################################### + +class ColRequiredOperator(Operator): + @classmethod + def poll(self, context): + wm = context.window_manager + active_col = wm.powerlib_props.active_col + return (active_col + and wm.powerlib_props.collections[active_col]) + + @staticmethod + def name_new_item(container, default_name): + if default_name not in container: + return default_name + else: + sorted_container = [] + for a in container: + if a.name.startswith(default_name + "."): + index = a.name[len(default_name) + 1:] + if index.isdigit(): + sorted_container.append(index) + sorted_container = sorted(sorted_container) + min_index = 1 + for num in sorted_container: + num = int(num) + if min_index < num: + break + min_index = num + 1 + return"{:s}.{:03d}".format(default_name, min_index) + + +class ColAndAssetRequiredOperator(ColRequiredOperator): + @classmethod + def poll(self, context): + if super().poll(context): + wm = context.window_manager + col = wm.powerlib_props.collections[wm.powerlib_props.active_col] + return (col.active_asset < len(col.assets) + and col.active_asset >= 0) + return False + + +class ASSET_OT_powerlib_reload_from_json(Operator): + bl_idname = "wm.powerlib_reload_from_json" + bl_label = "Reload from JSON" + bl_description = "Loads the library from the JSON file. Overrides non saved local edits!" + bl_options = {'UNDO', 'REGISTER'} + + @classmethod + def poll(self, context): + return True + + def execute(self, context): + from . import linking + import importlib + importlib.reload(linking) + + wm = context.window_manager + + wm.powerlib_props.collections.clear() + wm.powerlib_props.active_col = "" + runtime_vars["save_state"] = SaveState.AllSaved + + # Load single json library file + + library_path = bpy.path.abspath(context.scene.lib_path) + debug_print( + f"PowerLib2: Reading JSON library file from {library_path}") + + if not library_path: + debug_print("PowerLib2: ... no library path specified!") + runtime_vars["read_state"] = ReadState.NoFile + return {'FINISHED'} + + if not os.path.exists(library_path): + debug_print("PowerLib2: ... library filepath invalid!") + runtime_vars["read_state"] = ReadState.FilePathInvalid + return {'FINISHED'} + + library = {} + + with open(library_path) as data_file: + try: + library = json.load(data_file) + except (json.decoder.JSONDecodeError, KeyError, ValueError): + # malformed json data + debug_print( + "PowerLib2: ... JSON content is empty or malformed!") + runtime_vars["read_state"] = ReadState.FileContentInvalid + return {'FINISHED'} + + # Collections, eg. Characters + for collection_name in library: + asset_collection_prop = wm.powerlib_props.collections.add() + asset_collection_prop.name = collection_name + + # Assets, eg. Boris + collection = library[collection_name] + for asset_name in sorted(collection.keys()): + asset_json = collection[asset_name] + + asset_prop = asset_collection_prop.assets.add() + asset_prop.name = asset_name + + # Component Types, eg. instance_collections + for ctype_name, ctype_components in asset_json.items(): + ctype_prop = asset_prop.components_by_type.add() + ctype_prop.name = ctype_name + ctype_prop.component_type = ctype_prop.getComponentType( + ctype_name) + + # Individual components of this type, each with filepath and name + for filepath, name in ctype_components: + component_prop = ctype_prop.components.add() + component_prop.name = name + component_prop.id = name + component_prop.filepath = filepath + absolute_filepath = component_prop.absolute_filepath + if absolute_filepath: + bf_rel_fp = linking.relative_path_to_file( + component_prop.absolute_filepath) + else: + bf_rel_fp = '' + component_prop.filepath_rel = bf_rel_fp + + if library: + # Assign some collection by default (dictionaries are unordered) + wm.powerlib_props.active_col = next(iter(library.keys())) + + runtime_vars["read_state"] = ReadState.AllGood + else: + runtime_vars["read_state"] = ReadState.EmptyLib + + debug_print("PowerLib2: ... looks good!") + + return {'FINISHED'} + + +class ASSET_OT_powerlib_save_to_json(Operator): + bl_idname = "wm.powerlib_save_to_json" + bl_label = "Save to JSON" + bl_description = "Saves the edited library to the json file. Overrides the previous content!" + bl_options = {'UNDO', 'REGISTER'} + + @classmethod + def poll(self, context): + return True + + def execute(self, context): + wm = context.window_manager + + # Save properties to the JSON library file + + library_path = bpy.path.abspath(context.scene.lib_path) + debug_print(f"PowerLib2: Saving JSON library to file {library_path}") + + if not library_path or not os.path.exists(library_path): + debug_print("PowerLib2: ... invalid filepath! Could not save!") + self.report({'ERROR'}, "Invalid path! Could not save!") + return {'FINISHED'} + + collections_json_dict = {} + + # Collections, eg. Characters + for collection in wm.powerlib_props.collections: + assets_json_dict = {} + + # Assets, eg. Boris + for asset_name, asset_body in collection.assets.items(): + comps_by_type_json_dict = {} + + # Component Types, eg. instance_collections + for comp_type_name, comp_type_body in asset_body.components_by_type.items(): + comps_by_type_json_dict[comp_type_name] = [] + + # Individual components of this type, each with filepath and name + for i in comp_type_body.components: + comps_by_type_json_dict[comp_type_name].append([ + i.filepath, i.id + ]) + + assets_json_dict[asset_name] = comps_by_type_json_dict + collections_json_dict[collection.name] = assets_json_dict + + with open(library_path, 'w') as data_file: + json.dump(collections_json_dict, data_file, + indent=4, sort_keys=True,) + + runtime_vars["save_state"] = SaveState.AllSaved + debug_print("PowerLib2: ... no errors!") + return {'FINISHED'} + + +class ASSET_OT_powerlib_collection_rename(ColRequiredOperator): + bl_idname = "wm.powerlib_collection_rename" + bl_label = "Rename Category" + bl_description = "Rename the asset category" + bl_options = {'UNDO', 'REGISTER'} + + name: StringProperty(name="Name", description="Name of the collection") + + def invoke(self, context, event): + wm = context.window_manager + # fill in the field with the current value + self.name = wm.powerlib_props.collections[wm.powerlib_props.active_col].name + return wm.invoke_props_dialog(self) + + def execute(self, context): + wm = context.window_manager + col = wm.powerlib_props.collections[wm.powerlib_props.active_col] + col.name = self.name + wm.powerlib_props.active_col = self.name + + runtime_vars["save_state"] = SaveState.HasUnsavedChanges + return {'FINISHED'} + + +class ASSET_OT_powerlib_collection_add(Operator): + bl_idname = "wm.powerlib_collection_add" + bl_label = "Add Category" + bl_description = "Add a new asset category" + bl_options = {'UNDO', 'REGISTER'} + + name: StringProperty(name="Name", description="Name of the collection") + + def invoke(self, context, event): + wm = context.window_manager + return wm.invoke_props_dialog(self) + + def execute(self, context): + wm = context.window_manager + col = wm.powerlib_props.collections.add() + col.name = self.name + wm.powerlib_props.active_col = self.name + runtime_vars["save_state"] = SaveState.HasUnsavedChanges + return {'FINISHED'} + + +class ASSET_OT_powerlib_collection_del(ColRequiredOperator): + bl_idname = "wm.powerlib_collection_del" + bl_label = "Delete Category" + bl_description = "Delete the selected asset category" + bl_options = {'UNDO', 'REGISTER'} + + def execute(self, context): + wm = context.window_manager + idx = wm.powerlib_props.collections.find(wm.powerlib_props.active_col) + wm.powerlib_props.collections.remove(idx) + wm.powerlib_props.active_col = "" + runtime_vars["save_state"] = SaveState.HasUnsavedChanges + return {'FINISHED'} + + +class ASSET_OT_powerlib_assetitem_add(ColRequiredOperator): + bl_idname = "wm.powerlib_assetitem_add" + bl_label = "Add Asset" + bl_description = "Add a new asset to the selected category" + bl_options = {'UNDO', 'REGISTER'} + + def execute(self, context): + wm = context.window_manager + col = wm.powerlib_props.collections[wm.powerlib_props.active_col] + + asset = col.assets.add() + + # naming + asset.name = self.name_new_item(col.assets, "NewAsset") + + # select newly created asset + col.active_asset = len(col.assets) - 1 + + runtime_vars["save_state"] = SaveState.HasUnsavedChanges + return {'FINISHED'} + + +class ASSET_OT_powerlib_assetitem_del(ColAndAssetRequiredOperator): + bl_idname = "wm.powerlib_assetitem_del" + bl_label = "Delete Asset" + bl_description = "Delete the selected asset" + bl_options = {'UNDO', 'REGISTER'} + + def execute(self, context): + wm = context.window_manager + col = wm.powerlib_props.collections[wm.powerlib_props.active_col] + + col.assets.remove(col.active_asset) + + # change currently active asset + num_assets = len(col.assets) + if (col.active_asset > (num_assets - 1) and num_assets > 0): + col.active_asset = num_assets - 1 + + runtime_vars["save_state"] = SaveState.HasUnsavedChanges + return {'FINISHED'} + + +class ASSET_OT_powerlib_component_add(ColAndAssetRequiredOperator): + bl_idname = "wm.powerlib_component_add" + bl_label = "Add Asset Component" + bl_description = "Add a new component to the selected asset" + bl_options = {'UNDO', 'REGISTER'} + + component_type: enum_component_type + needs_select: BoolProperty(default=False, options={'HIDDEN'}) + + def invoke(self, context, event): + wm = context.window_manager + if self.needs_select: + self.needs_select = False + return wm.invoke_props_dialog(self) + else: + return self.execute(context) + + def execute(self, context): + wm = context.window_manager + + asset_collection = wm.powerlib_props.collections[wm.powerlib_props.active_col] + active_asset = asset_collection.assets[asset_collection.active_asset] + + # create container for type if it does not exist yet + components_of_type = active_asset.components_by_type.get( + self.component_type.lower()) + if components_of_type is None: + components_of_type = active_asset.components_by_type.add() + components_of_type.name = self.component_type.lower() + components_of_type.component_type = self.component_type + + component = components_of_type.components.add() + + # naming + component.name = self.name_new_item( + components_of_type.components, "NewComponent") + + # select newly created component + components_of_type.active_component = len( + components_of_type.components) - 1 + + runtime_vars["save_state"] = SaveState.HasUnsavedChanges + return {'FINISHED'} + + +class ASSET_OT_powerlib_component_del(ColAndAssetRequiredOperator): + bl_idname = "wm.powerlib_component_del" + bl_label = "Delete Asset Component" + bl_description = "Delete the selected asset component" + bl_options = {'UNDO', 'REGISTER'} + + component_type: enum_component_type + + def execute(self, context): + wm = context.window_manager + + asset_collection = wm.powerlib_props.collections[wm.powerlib_props.active_col] + active_asset = asset_collection.assets[asset_collection.active_asset] + + # ignore if container for type does not exist + components_of_type = active_asset.components_by_type.get( + self.component_type.lower()) + if components_of_type is None: + return {'FINISHED'} + + components_of_type.components.remove( + components_of_type.active_component) + + num_components = len(components_of_type.components) + # if this component type list is empty, delete + if num_components == 0: + idx = active_asset.components_by_type.find( + self.component_type.lower()) + active_asset.components_by_type.remove(idx) + # change currently active component + elif (components_of_type.active_component > (num_components - 1) and num_components > 0): + components_of_type.active_component = num_components - 1 + + runtime_vars["save_state"] = SaveState.HasUnsavedChanges + return {'FINISHED'} + + +class AssetFiles(): + def __init__(self): + self._components = {} + self._files = {} + + @staticmethod + def get_nested_array(_dict, key, array): + if key not in _dict: + _dict[key] = array() + return _dict[key] + + def get_component(self, component_type): + return self.get_nested_array(self._components, component_type, dict) + + def add(self, component_type, filepath, _id): + """Populate the dictionary of lists""" + _component = self.get_component(component_type) + _file = self.get_nested_array(_component, filepath, list) + _file.append(_id) + + def process(self): + """handle the importing""" + callbacks = { + 'COLLECTION_REFERENCE_OBJECTS': linking.load_collection_reference_objects, + 'NON_INSTANCE_COLLECTIONS': linking.load_non_instance_collections, + 'INSTANCE_COLLECTIONS': linking.load_instance_collections, + } + + for _component, _files in self._components.items(): + callback = callbacks.get(_component) + + assert callback, f"Component \"{_component}\" not supported" + + for _file, ids in _files.items(): + callback(_file, ids) + + +class ASSET_OT_powerlib_link_in_component(ColAndAssetRequiredOperator): + bl_idname = "wm.powerlib_link_in_component" + bl_label = "Add component to scene" + bl_description = "Add component to scene" + bl_options = {'UNDO', 'REGISTER'} + + index: IntProperty( + default=-1, + options={'HIDDEN', 'SKIP_SAVE'}, + ) + + def execute(self, context): + from . import linking + if "bpy" in locals(): + import importlib + importlib.reload(linking) + + wm = context.window_manager + + asset_collection = wm.powerlib_props.collections[wm.powerlib_props.active_col] + + if self.index == -1: + active_asset = asset_collection.assets[asset_collection.active_asset] + else: + active_asset = asset_collection.assets[self.index] + + debug_print(f'Linking in {active_asset.name}') + + files = AssetFiles() + + for component_list in active_asset.components_by_type: + component_type = component_list.component_type + + for component in component_list.components: + files.add(component_type, + component.absolute_filepath, component.id) + + files.process() + + return {'FINISHED'} + + +# Panel ####################################################################### + +class ASSET_UL_asset_components(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + is_edit_mode = context.window_manager.powerlib_props.is_edit_mode + col = layout.split() + col.enabled = is_edit_mode + col.prop(item, "filepath_rel", text="", emboss=is_edit_mode) + col.prop_search(item, "id", item, "collections", text="") + + +class ASSET_UL_collection_assets(UIList): + def draw_item(self, context, layout, data, set, icon, active_data, active_propname, index): + # layout.prop(set, "name", text="", icon='LINK_BLEND', emboss=False) + is_edit_mode = context.window_manager.powerlib_props.is_edit_mode + col = layout.split() + col.prop(set, "name", text="", icon='LINK_BLEND', emboss=False) + if is_edit_mode: + return + col = layout.split() + col.enabled = True + plus = col.operator( + "wm.powerlib_link_in_component", text="", icon='IMPORT') + plus.index = index + + +class ASSET_PT_powerlib(Panel): + bl_label = 'Powerlib' # panel section name + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_category = 'Powerlib' # tab name + + @classmethod + def poll(cls, context): + # restrict availability? Yes, only object mode + return context.mode == 'OBJECT' + + def draw_header(self, context): + wm = context.window_manager + + row = self.layout.row(align=True) + row.prop(wm.powerlib_props, "is_edit_mode", + text="", + icon='TOOL_SETTINGS') + row.operator("wm.powerlib_reload_from_json", + text="", icon='FILE_REFRESH') + + def draw(self, context): + wm = context.window_manager + scene = context.scene + + is_edit_mode = wm.powerlib_props.is_edit_mode + layout = self.layout + + # Setting for the JSON library path + + if is_edit_mode: + row = layout.row() + row.prop(scene, "lib_path", text="Library Path") + layout.separator() + + # Fail report for library loading + + read_state = runtime_vars["read_state"] + + if (read_state != ReadState.AllGood + and not (read_state == ReadState.EmptyLib and is_edit_mode)): + + row = layout.row() + row.alignment = 'CENTER' + if (read_state == ReadState.NotLoaded or read_state == ReadState.NoFile): + if is_edit_mode: + row.label(text="No library path loaded", icon='ERROR') + else: + row.alignment = 'EXPAND' + row.label(text="Choose a library path:") + row = layout.row() + row.prop(scene, "lib_path", text="") + elif (read_state == ReadState.FilePathInvalid): + if not is_edit_mode: + row.alignment = 'EXPAND' + row.label(text="Choose a library path:") + row = layout.row() + row.prop(scene, "lib_path", text="") + row = layout.row() + row.label(text="Can not find a library in the given path", + icon='ERROR') + elif (read_state == ReadState.FileContentInvalid): + row.label(text="The library is empty or corrupt!", icon='ERROR') + elif (read_state == ReadState.EmptyLib and not is_edit_mode): + row.label(text="The chosen library is empty") + + return + + # Category selector + + row = layout.row(align=True) + + row.prop_search( + wm.powerlib_props, "active_col", # Currently active + wm.powerlib_props, "collections", # Collection to search + text="", icon="MENU_PANEL" # UI icon and label + ) + if is_edit_mode: + row.operator("wm.powerlib_collection_rename", + text="", icon='OUTLINER_DATA_FONT') + row.operator("wm.powerlib_collection_add", text="", icon='ADD') + row.operator("wm.powerlib_collection_del", text="", icon='REMOVE') + + # UI List with the assets of the selected category + + row = layout.row() + if (wm.powerlib_props.active_col): + asset_collection = wm.powerlib_props.collections[wm.powerlib_props.active_col] + row.template_list( + "ASSET_UL_collection_assets", "", # type and unique id + asset_collection, "assets", # pointer to the CollectionProperty + asset_collection, "active_asset", # pointer to the active identifier + rows=6, + ) + # add/remove/specials UI list Menu + if is_edit_mode: + col = row.column(align=True) + col.operator("wm.powerlib_assetitem_add", + icon='ADD', text="") + col.operator("wm.powerlib_assetitem_del", + icon='REMOVE', text="") + # col.menu("ASSET_MT_powerlib_assetlist_specials", icon='DOWNARROW_HLT', text="") + else: + row.enabled = False + row.label(text="Choose an Asset Collection!") + + # Properties and Components of this Asset + + if wm.powerlib_props.active_col: + layout.separator() + + if asset_collection.assets: + active_asset = asset_collection.assets[asset_collection.active_asset] + + for components_of_type in active_asset.components_by_type: + row = layout.row() + name, icon = enum_item_name_icon( + enum_component_type, components_of_type.component_type) + row.label(text=name, icon=icon) + + row = layout.row() + row.template_list( + "ASSET_UL_asset_components", # type + "components_of_type.component_type", # unique id + components_of_type, "components", # pointer to the CollectionProperty + components_of_type, "active_component", # pointer to the active identifier + rows=2, + ) + # add/remove/specials UI list Menu + if is_edit_mode: + col = row.column(align=True) + col.operator("wm.powerlib_component_add", icon='ADD', + text="").component_type = components_of_type.component_type + col.operator("wm.powerlib_component_del", icon='REMOVE', + text="").component_type = components_of_type.component_type + + if is_edit_mode: + layout.separator() + row = layout.row() + row.operator("wm.powerlib_component_add", + icon='ADD').needs_select = True + + # Save + + if is_edit_mode: + layout.separator() + row = layout.row() + row.operator("wm.powerlib_save_to_json", + icon='ERROR' if runtime_vars["save_state"] == SaveState.HasUnsavedChanges else 'FILE_TICK') + + +# Registry #################################################################### + +classes = ( + ComponentItem, + Component, + ComponentsList, + AssetItem, + AssetCollection, + PowerProperties, + ASSET_UL_asset_components, + ASSET_UL_collection_assets, + ASSET_PT_powerlib, + ASSET_OT_powerlib_reload_from_json, + ASSET_OT_powerlib_save_to_json, + ASSET_OT_powerlib_collection_rename, + ASSET_OT_powerlib_collection_add, + ASSET_OT_powerlib_collection_del, + ASSET_OT_powerlib_assetitem_add, + ASSET_OT_powerlib_assetitem_del, + ASSET_OT_powerlib_component_add, + ASSET_OT_powerlib_component_del, + ASSET_OT_powerlib_link_in_component, +) + + +def powerlib_lib_path_update_cb(self, context): + debug_print("PowerLib2: Loading Add-on and Library") + bpy.ops.wm.powerlib_reload_from_json() + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bpy.types.WindowManager.powerlib_props = PointerProperty( + name="Powerlib Add-on Properties", + description="Properties and data used by the Powerlib Add-on", + type=PowerProperties, + ) + + bpy.types.Scene.lib_path = StringProperty( + name="Powerlib Add-on Library Path", + description="Path to a PowerLib JSON file", + subtype='FILE_PATH', + update=powerlib_lib_path_update_cb, + ) + + +def unregister(): + del bpy.types.Scene.lib_path + del bpy.types.WindowManager.powerlib_props + + for cls in classes: + bpy.utils.unregister_class(cls) + + +if __name__ == "__main__": + register() diff --git a/linking.py b/linking.py new file mode 100644 index 0000000..b698b20 --- /dev/null +++ b/linking.py @@ -0,0 +1,184 @@ +import os +import bpy + + +VERBOSE = False # enable this for debugging + + +def debug_print(*args): + """Print debug messages""" + if VERBOSE: + print(*args) + + +def relative_path_to_file(filepath): + """Makes a path relative to the current file""" + return bpy.path.relpath(filepath) + + +def absolute_path_from_file(rel_filepath): + return bpy.path.abspath(rel_filepath) + + +def relative_path_to_lib(filepath): + """Makes a path relative to the current library""" + filepath = absolute_path_from_file(filepath) + libpath = os.path.dirname( + absolute_path_from_file(bpy.context.scene['lib_path'])) + rel_path = os.path.relpath(filepath, libpath) + return rel_path + + +def bottom_up_from_idblock(idblock): + """Generator, yields datablocks from the bottom (i.e. uses nothing) upward. + + Stupid in that it doesn't detect cycles yet. + + :param idblock: the idblock whose users to yield. + """ + + visited = set() + + def visit(idblock): + # Prevent visiting the same idblock multiple times + if idblock in visited: + return + visited.add(idblock) + + user_map = bpy.data.user_map([idblock]) + # There is only one entry here, for the idblock we requested. + for user in user_map[idblock]: + yield from visit(user) + yield idblock + + yield from visit(idblock) + + +def make_local(ob): + # make local like a boss (using the patch from Sybren Stuvel) + for idblock in bottom_up_from_idblock(ob): + + if idblock.library is None: + # Already local + continue + + debug_print('Should make %r local: ' % idblock) + debug_print(' - result: %s' % idblock.make_local(clear_proxy=True)) + + # this shouldn't happen, but it does happen :/ + if idblock.library: + pass + + +def treat_ob(ob, grp): + """Remap existing ob to the new ob""" + ob_name = ob.name + debug_print(f'Processing {ob_name}') + + try: + existing = bpy.data.objects[ob_name, None] + + except KeyError: + debug_print('Not yet in Blender, just linking to scene.') + bpy.context.scene.collection.objects.link(ob) + + make_local(ob) + ob = bpy.data.objects[ob_name, None] + + debug_print('GRP: ', grp.name) + grp.objects.link(ob) + + else: + debug_print(f'Updating {ob.name}') + # when an object already exists: + # - find local version + # - user_remap() it + existing.user_remap(ob) + existing.name = f'(PRE-SPLODE LOCAL) {existing.name}' + + # Preserve visible or hidden state + ob.hide_viewport = existing.hide_viewport + + # Preserve animation (used to place the instance in the scene) + if existing.animation_data: + ob.animation_data_create() + ob.animation_data.action = existing.animation_data.action + + bpy.data.objects.remove(existing) + make_local(ob) + + +def load_collection_reference_objects(filepath, collection_names): + # We load one collection at a time + debug_print(f'Loading collections {filepath} : {collection_names}') + rel_path = relative_path_to_file(filepath) + + # Road a object scene we know the name of. + with bpy.data.libraries.load(rel_path, link=True) as (data_from, data_to): + data_to.collections = collection_names + + data = {} + for collection in data_to.collections: + debug_print(f'Handling collection {collection.name}') + ref_collection_name = f'__REF{collection.name}' + + if ref_collection_name in bpy.data.collections: + object_names_from = [ob.name for ob in collection.objects] + object_names_to = [ + ob.name for ob in bpy.data.collections[ref_collection_name].objects] + object_names_diff = list( + set(object_names_to) - set(object_names_from)) + + # Delete removed objects + for ob in object_names_diff: + # bpy.data.objects[ob].select_set(True) + # bpy.ops.object.delete() + bpy.data.objects.remove(bpy.data.objects[ob]) + else: + bpy.ops.collection.create(name=ref_collection_name) + + # store all the objects that are in the collection + data[bpy.data.collections[ref_collection_name]] = [ + ob for ob in collection.objects] + + # remove the collections + bpy.data.collections.remove(collection, do_unlink=True) + + # add the new objects and make them local + process_collection_reference_objects(data) + + +def process_collection_reference_objects(data): + for collection, objects in data.items(): + for ob in objects: + treat_ob(ob, collection) + + +def load_instance_collections(filepath, collection_names): + debug_print(f'Loading collections {filepath} : {collection_names}') + rel_path = relative_path_to_file(filepath) + + # Load an object scene we know the name of. + with bpy.data.libraries.load(rel_path, link=True) as (data_from, data_to): + data_to.collections = collection_names + + scene = bpy.context.scene + for collection in collection_names: + instance = bpy.data.objects.new(collection.name, None) + instance.instance_type = 'COLLECTION' + instance.empty_display_size = 0.01 + instance.instance_collection = collection + scene.collection.objects.link(instance) + + +def load_non_instance_collections(filepath, collection_names): + debug_print(f'Loading collections {filepath} : {collection_names}') + rel_path = relative_path_to_file(filepath) + + # Load an object scene we know the name of. + with bpy.data.libraries.load(rel_path, link=True) as (data_from, data_to): + data_to.collections = collection_names + + scene = bpy.context.scene + for collection in collection_names: + scene.collection.children.link(collection)