Assets: Make remote library listing work with custom properties

The remote library listing already included custom properties. However,
the format they used didn't match what the C++ indexer expected at all.
This means the custom properties would essentially get lost. Sometimes
they contain important information though, for example, the type of node
tree so the compositor asset shelf can display only compositing assets.

The C++ indexer expected the same format as for the local asset index.
This avoids duplicating the reading code. This commit makes the listing
match that.

Another issue is, that a top level custom property should always be a
group property (this is the one iterated within when doing
`bpy.types.AssetData.items(), like the listing generator does).
Otherwise adding more properties isn't supported, and property lookups
assumed this. We can handle that by letting the indexer create the top
level group if needed, that way it can be implicit in the remote library
listing.

Pull Request: https://projects.blender.org/blender/blender/pulls/156305
This commit is contained in:
Julian Eisel 2026-03-31 17:11:34 +02:00 • committed by Julian Eisel
parent bc89fff217
commit 5556e04509
6 changed files with 76 additions and 48 deletions

View file

@ -29,13 +29,13 @@ type AssetIDTypeV1 = str
class CustomPropertyTypeV1(StrEnum):
STRING = "STRING"
INT = "INT"
FLOAT = "FLOAT"
ARRAY = "ARRAY"
GROUP = "GROUP"
DOUBLE = "DOUBLE"
BOOLEAN = "BOOLEAN"
IDP_STRING = "IDP_STRING"
IDP_INT = "IDP_INT"
IDP_FLOAT = "IDP_FLOAT"
IDP_ARRAY = "IDP_ARRAY"
IDP_GROUP = "IDP_GROUP"
IDP_DOUBLE = "IDP_DOUBLE"
IDP_BOOLEAN = "IDP_BOOLEAN"
@dataclass
@ -96,14 +96,15 @@ class AssetMetadataV1:
description: str | None = None
license: str | None = None
copyright: str | None = None
custom: CustomPropertiesV1 | None = None
properties: CustomPropertiesV1 | None = None
type CustomPropertiesV1 = dict[str, CustomPropertyV1]
type CustomPropertiesV1 = list[CustomPropertyV1]
@dataclass
class CustomPropertyV1:
name: str
type: CustomPropertyTypeV1
value: CustomPropertiesV1 | list[Any] | float | int | str | bool
itemtype: CustomPropertyTypeV1 | None = None

View file

@ -287,14 +287,14 @@ components:
"description": { type: string }
"license": { type: string }
"copyright": { type: string }
"custom": { $ref: "#/components/schemas/CustomPropertiesV1" }
"properties": { $ref: "#/components/schemas/CustomPropertiesV1" }
CustomPropertiesV1:
type: object
type: array
items:
$ref: "#/components/schemas/CustomPropertyV1"
description: >
Arbitrary custom properties of the asset. Keys are the property names.
additionalProperties:
$ref: "#/components/schemas/CustomPropertyV1"
CustomPropertyV1:
type: object
@ -304,6 +304,7 @@ components:
as `CustomPropertiesV1` object again. Arrays should specify an
`itemtype`.
properties:
"name": { type: string }
"type": { $ref: "#/components/schemas/CustomPropertyTypeV1" }
"itemtype": { $ref: "#/components/schemas/CustomPropertyTypeV1" }
"value":
@ -314,14 +315,23 @@ components:
- { type: integer }
- { type: string }
- { type: boolean }
required: [type, value]
required: [name, type, value]
CustomPropertyTypeV1:
type: string
description: >
Type of IDProperty without the `IDP_` prefix, see `eIDPropertyType` in `DNA_ID_enumms.h`.
For now, type `ID` and `IDPARRAY` are not supported.
enum: [STRING, INT, FLOAT, ARRAY, GROUP, DOUBLE, BOOLEAN]
Type of IDProperty, see `eIDPropertyType` in `DNA_ID_enumms.h`. For now,
type `ID` and `IDPARRAY` are not supported.
enum:
[
IDP_STRING,
IDP_INT,
IDP_FLOAT,
IDP_ARRAY,
IDP_GROUP,
IDP_DOUBLE,
IDP_BOOLEAN,
]
CatalogV1:
type: object

View file

@ -144,37 +144,40 @@ def _get_asset_meta(asset_data: bpy.types.AssetData) -> api_models.AssetMetadata
# Convert custom properties.
import rna_prop_ui
custom_props: api_models.CustomPropertiesV1 = {}
custom_props: api_models.CustomPropertiesV1 = []
for prop_name, prop_value in asset_data.items():
is_array = isinstance(prop_value, rna_prop_ui.ARRAY_TYPES) and len(prop_value) > 0
item_value = prop_value[0] if is_array else prop_value
match item_value:
case bool():
value_type = api_models.CustomPropertyTypeV1.BOOLEAN
value_type = api_models.CustomPropertyTypeV1.IDP_BOOLEAN
case int():
value_type = api_models.CustomPropertyTypeV1.INT
value_type = api_models.CustomPropertyTypeV1.IDP_INT
case str():
value_type = api_models.CustomPropertyTypeV1.STRING
value_type = api_models.CustomPropertyTypeV1.IDP_STRING
case float():
value_type = api_models.CustomPropertyTypeV1.FLOAT
value_type = api_models.CustomPropertyTypeV1.IDP_FLOAT
case _:
# Unsupported type, just ignore it.
continue
if is_array:
custom_prop = api_models.CustomPropertyV1(
type=api_models.CustomPropertyTypeV1.ARRAY,
name=prop_name,
type=api_models.CustomPropertyTypeV1.IDP_ARRAY,
value=list(prop_value),
itemtype=value_type,
)
else:
custom_prop = api_models.CustomPropertyV1(type=value_type, value=prop_value)
custom_prop = api_models.CustomPropertyV1(
name=prop_name, type=value_type, value=prop_value
)
custom_props[prop_name] = custom_prop
custom_props.append(custom_prop)
if custom_props:
meta.custom = custom_props
meta.properties = custom_props
if meta == api_models.AssetMetadataV1():
return None

View file

@ -469,7 +469,9 @@ std::unique_ptr<io::serialize::ArrayValue> convert_to_serialize_values(
const IDProperty *properties);
/**
* \brief Convert the given `value` to an `IDProperty`.
* \brief Convert the given `value` (which must be an array) to an `IDProperty`.
* \return The first successfully converted property with further array elements in the
* `IDProperty.next` chain, or null if none could be converted or \a value is not an array.
*/
IDProperty *convert_from_serialize_value(const io::serialize::Value &value);

View file

@ -252,7 +252,19 @@ AssetMetaData *asset_metadata_from_dictionary(const DictionaryValue &entry)
}
if (const std::shared_ptr<Value> *value = entry.lookup(ATTRIBUTE_ENTRIES_PROPERTIES)) {
asset_data->properties = convert_from_serialize_value(**value);
IDProperty *properties = convert_from_serialize_value(**value);
/* The top level property must be a group, further asset metadata property lookups assume
* that. This is also the only way to support more than a single property. */
if (properties->next || properties->type != IDP_GROUP) {
asset_data->properties = bke::idprop::create_group("AssetMetaData.properties").release();
for (IDProperty *property = properties; property != nullptr; property = property->next) {
IDP_AddToGroup(asset_data->properties, property);
}
}
else {
asset_data->properties = properties;
}
}
return asset_data;

View file

@ -45,19 +45,19 @@ class CustomPropertiesTest(unittest.TestCase):
Types = api_models.CustomPropertyTypeV1
Prop = api_models.CustomPropertyV1
expected_custom = {
'amazing': Prop(type=Types.BOOLEAN, value=True),
'barcode': Prop(type=Types.STRING, value='155366'),
'count': Prop(type=Types.INT, value=47),
'location': Prop(type=Types.STRING, value='café'),
'size': Prop(type=Types.FLOAT, value=32.7),
'dimensions': Prop(type=Types.ARRAY,
value=[2.0, 2.0, 2.0],
itemtype=Types.FLOAT),
}
# autopep8: off
expected_props = [
Prop(name='dimensions', type=Types.IDP_ARRAY, value=[2.0, 2.0, 2.0], itemtype=Types.IDP_FLOAT),
Prop(name='barcode', type=Types.IDP_STRING, value='155366'),
Prop(name='location', type=Types.IDP_STRING, value='café'),
Prop(name='size', type=Types.IDP_FLOAT, value=32.7),
Prop(name='count', type=Types.IDP_INT, value=47),
Prop(name='amazing', type=Types.IDP_BOOLEAN, value=True),
]
# autopep8: on
assert meta is not None
self.assertEqual(expected_custom, meta.custom)
self.assertEqual(expected_props, meta.properties)
def test_array_properties(self) -> None:
asset_data = self.cube.asset_data
@ -70,17 +70,17 @@ class CustomPropertiesTest(unittest.TestCase):
Types = api_models.CustomPropertyTypeV1
Prop = api_models.CustomPropertyV1
expected_custom = {
'agents': Prop(type=Types.ARRAY, value=["007", "47", "327"], itemtype=Types.STRING),
'locations': Prop(type=Types.ARRAY, value=["Hokkaido", "Santa Fortuna", "Sapienza"], itemtype=Types.STRING),
'boundingbox': Prop(type=Types.ARRAY, value=[-3.0, -4.0, -0.1, 1.0, 2.0, 3.0], itemtype=Types.FLOAT),
'dimensions': Prop(type=Types.ARRAY,
value=[2.0, 2.0, 2.0],
itemtype=Types.FLOAT),
}
# autopep8: off
expected_prop = [
Prop(name='dimensions', type=Types.IDP_ARRAY, value=[2.0, 2.0, 2.0], itemtype=Types.IDP_FLOAT),
Prop(name='agents', type=Types.IDP_ARRAY, value=["007", "47", "327"], itemtype=Types.IDP_STRING),
Prop(name='locations', type=Types.IDP_ARRAY, value=["Hokkaido", "Santa Fortuna", "Sapienza"], itemtype=Types.IDP_STRING),
Prop(name='boundingbox', type=Types.IDP_ARRAY, value=[-3.0, -4.0, -0.1, 1.0, 2.0, 3.0], itemtype=Types.IDP_FLOAT),
]
# autopep8: on
assert meta is not None
self.assertEqual(expected_custom, meta.custom)
self.assertEqual(expected_prop, meta.properties)
def test_serialize_to_json(self) -> None:
meta = asset_finder._get_asset_meta(self.cube.asset_data)