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.
This commit is contained in:
@@ -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.
|
||||
|
||||
942
__init__.py
Normal file
942
__init__.py
Normal file
@@ -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 *****
|
||||
|
||||
# <pep8 compliant>
|
||||
|
||||
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()
|
||||
184
linking.py
Normal file
184
linking.py
Normal file
@@ -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)
|
||||
Reference in New Issue
Block a user