it looks for a folder called "asset_previews" next to the library file (assets.json) currently the name of the asset needs to match the name of the PNG. lowercase and with underscores instead of spaces
990 lines
34 KiB
Python
990 lines
34 KiB
Python
# ***** 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'}
|
|
|
|
# Define new preview collection
|
|
pcoll = preview_collections["powerlib"]
|
|
pcoll.clear()
|
|
json_folder, json_file = os.path.split(library_path)
|
|
plib_previews_dir = os.path.join(
|
|
json_folder, "asset_previews")
|
|
|
|
# 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
|
|
|
|
# Load preview for asset in subfolder /asset_previews
|
|
# turn lowercase and replace spaces with underscore
|
|
preview_name = f"{asset_prop.name.lower()}.png".replace(
|
|
" ", "_")
|
|
|
|
# Clear preview if it already exists
|
|
if preview_name not in pcoll:
|
|
preview_path = os.path.join(
|
|
plib_previews_dir, preview_name)
|
|
|
|
# Only load a preview if it exists
|
|
if os.path.exists(preview_path):
|
|
pcoll.load(asset_prop.name, preview_path, 'IMAGE')
|
|
|
|
preview_collections["powerlib"] = pcoll
|
|
|
|
# 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()
|
|
|
|
# Use preview icon for the asset if available
|
|
pcoll = preview_collections["powerlib"]
|
|
if set.name in pcoll:
|
|
asset_thumb = pcoll[set.name].icon_id
|
|
col.prop(set, "name", text="",
|
|
icon_value=asset_thumb, emboss=False)
|
|
else:
|
|
col.prop(set, "name", text="",
|
|
icon='LINKED', 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()
|
|
|
|
|
|
# Store custom previews in a new preview collection
|
|
preview_collections = {}
|
|
|
|
|
|
def register():
|
|
import bpy.utils.previews
|
|
pcoll = bpy.utils.previews.new()
|
|
pcoll.plib_previews_dir = ""
|
|
|
|
preview_collections["powerlib"] = pcoll
|
|
|
|
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)
|
|
|
|
for pcoll in preview_collections.values():
|
|
bpy.utils.previews.remove(pcoll)
|
|
preview_collections.clear()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
register()
|