Python API: Direct access to attribute arrays of meshes, curves, point clouds and GP drawings #122091

Open
Sietse Brouwer wants to merge 5 commits from SietseB/blender:core-rna-python-attribute-array into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
12 changed files with 1379 additions and 2 deletions

View File

@ -42,9 +42,10 @@ typedef struct CustomDataLayer {
int active_mask;
/** Shape key-block unique id reference. */
int uid;
/** Only for use in RNA and #bpy_rna: number of items in the layer data. */
int length;
Review

I think it's important to avoid adding this. It should be retrieved from the "context" as necessary. That sort of redundant storage adds plenty of problems with syncing and invalid states.

I think it's important to avoid adding this. It should be retrieved from the "context" as necessary. That sort of redundant storage adds plenty of problems with syncing and invalid states.
Review

The 'context' is the problem here. The function that creates the Python object only receives a PointerRNA *ptr, which contains an ID owner_id and void *data. For meshes and curves, we can derive the length of the attribute array from the owner_id, but for Grease Pencil we can't, because the geometry is in a Drawing, which isn't an ID type.
I see the problems you mention, of course, but I don't see a better solution for now. Any ideas, perhaps?

The 'context' is the problem here. The function that creates the Python object only receives a `PointerRNA *ptr`, which contains an `ID owner_id` and `void *data`. For meshes and curves, we can derive the length of the attribute array from the `owner_id`, but for Grease Pencil we can't, because the geometry is in a `Drawing`, which isn't an `ID` type. I see the problems you mention, of course, but I don't see a better solution for now. Any ideas, perhaps?
Review

There was some discussion on improving the state of things regarding ownership of data pointed at by PointerRNA. See #122431

There was some discussion on improving the state of things regarding ownership of data pointed at by `PointerRNA`. See https://projects.blender.org/blender/blender/issues/122431

I think storing the array length in the custom data layer is not that bad, if this was a C++ data type this would be a Vector and so it would have the size too. But if this is done, I think it requires some refactoring in customdata.cc to ensure any array pointer changes go along with size changes. Maybe affects some public API functions as well, where maybe it needs to then pass an Array instead of a pointer.

I think storing the array length in the custom data layer is not that bad, if this was a C++ data type this would be a `Vector` and so it would have the size too. But if this is done, I think it requires some refactoring in customdata.cc to ensure any array pointer changes go along with size changes. Maybe affects some public API functions as well, where maybe it needs to then pass an Array instead of a pointer.
Review

if this was a C++ data type this would be a Vector

Not quite. ImplicitSharingInfo serves as the ownership here. data just serves as quick access, pointing to any contiguous array. At the very least it should be stored in CustomData, not CustomDataLayer.

Anyway, improvements to PointerRNA should make this change unnecessary.

>if this was a C++ data type this would be a Vector Not quite. `ImplicitSharingInfo` serves as the ownership here. `data` just serves as quick access, pointing to any contiguous array. At the very least it should be stored in `CustomData`, not `CustomDataLayer`. Anyway, improvements to `PointerRNA` should make this change unnecessary.

I think you'd still have some way of freeing the data in a destructor, which for some data types requires the length. To me it's strange for a data structure to not know the length of its own array members, and instead passing it into various CustomData_ API functions.

I think you'd still have some way of freeing the data in a destructor, which for some data types requires the length. To me it's strange for a data structure to not know the length of its own array members, and instead passing it into various `CustomData_` API functions.
Review

ImplicitSharingInfo handles the destruction itself. All CustomData should have to do is decrement the user count. But your overall point is fair.

`ImplicitSharingInfo` handles the destruction itself. All `CustomData` should have to do is decrement the user count. But your overall point is fair.
/** Layer name, MAX_CUSTOMDATA_LAYER_NAME. */
char name[68];
char _pad1[4];
/** Layer data. */
void *data;
/**

View File

@ -84,6 +84,7 @@ const StructRNA *RNA_struct_base_child_of(const StructRNA *type, const StructRNA
bool RNA_struct_is_ID(const StructRNA *type);
bool RNA_struct_is_a(const StructRNA *type, const StructRNA *srna);
bool RNA_struct_is_attribute_array(const StructRNA *type);
bool RNA_struct_undo_check(const StructRNA *type);

View File

@ -723,6 +723,8 @@ enum StructFlag {
* So accessing the property should not read from the current context to derive values/limits.
*/
STRUCT_NO_CONTEXT_WITHOUT_OWNER_ID = (1 << 11),
/** Indicates that this struct is a geometry attribute array. */
STRUCT_ATTRIBUTE_ARRAY = (1 << 12),
};
using StructValidateFunc = int (*)(PointerRNA *ptr, void *data, bool *have_function);

View File

@ -744,6 +744,11 @@ bool RNA_struct_idprops_unset(PointerRNA *ptr, const char *identifier)
return false;
}
bool RNA_struct_is_attribute_array(const StructRNA *type)
{
return (type->flag & STRUCT_ATTRIBUTE_ARRAY) != 0;
}
bool RNA_struct_is_a(const StructRNA *type, const StructRNA *srna)
{
const StructRNA *base;

View File

@ -351,6 +351,13 @@ static void rna_Attribute_update_data(Main * /*bmain*/, Scene * /*scene*/, Point
}
}
static PointerRNA rna_Attribute_attribute_array_get(PointerRNA *ptr)
{
CustomDataLayer *layer = static_cast<CustomDataLayer *>(ptr->data);
layer->length = rna_Attribute_data_length(ptr);
return rna_pointer_inherit_refine(ptr, &RNA_AttributeArray, ptr->data);
}
/* Color Attribute */
static void rna_ByteColorAttributeValue_color_get(PointerRNA *ptr, float *values)
@ -1175,6 +1182,17 @@ static void rna_def_attribute_float2(BlenderRNA *brna)
RNA_def_property_update(prop, 0, "rna_Attribute_update_data");
}
static void rna_def_attribute_array(BlenderRNA *brna)
{
/* The #AttributeArray struct grants access to attribute arrays in Blender python. The
* #STRUCT_ATTRIBUTE_ARRAY flag is linked to the #BPy_AttributeArray in #bpy_rna.cc. See
* #python/generic/attribute_array_py_api.cc for all Python methods available in this class. */
StructRNA *srna = RNA_def_struct(brna, "AttributeArray", nullptr);
RNA_def_struct_flag(srna, STRUCT_ATTRIBUTE_ARRAY);
RNA_def_struct_sdna(srna, "CustomDataLayer");
RNA_def_struct_ui_text(srna, "Attribute Array", "A geometry attribute data array");
}
static void rna_def_attribute(BlenderRNA *brna)
{
PropertyRNA *prop;
@ -1217,6 +1235,12 @@ static void rna_def_attribute(BlenderRNA *brna)
RNA_def_property_ui_text(prop, "Is Required", "Whether the attribute can be removed or renamed");
RNA_def_property_clear_flag(prop, PROP_EDITABLE);
/* Direct Python access to the attribute array. */
prop = RNA_def_property(srna, "data_array", PROP_POINTER, PROP_NONE);
RNA_def_property_struct_type(prop, "AttributeArray");
RNA_def_property_pointer_funcs(
prop, "rna_Attribute_attribute_array_get", nullptr, nullptr, nullptr);
/* types */
rna_def_attribute_float(brna);
rna_def_attribute_float_vector(brna);
@ -1230,6 +1254,8 @@ static void rna_def_attribute(BlenderRNA *brna)
rna_def_attribute_bool(brna);
rna_def_attribute_float2(brna);
rna_def_attribute_int8(brna);
rna_def_attribute_array(brna);
}
/* Mesh/PointCloud/Curves.attributes */

View File

@ -14,6 +14,7 @@ set(INC_SYS
)
set(SRC
attribute_array_py_api.cc
bgl.cc
bl_math_py_api.cc
blf_py_api.cc
@ -24,6 +25,8 @@ set(SRC
py_capi_rna.cc
py_capi_utils.cc
attribute_array_py_api.h
attribute_array_py_types.hh
bgl.h
bl_math_py_api.h
blf_py_api.h

View File

@ -0,0 +1,659 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup pygen
*/
#include "attribute_array_py_types.hh"
#include "RNA_access.hh"
namespace blender::python::attributearray {
/* -------------------------------------------------------------------- */
/** \name BPy_AttributeArray Mapping
* \{ */
static int BPy_AttributeArray_assign_subscript_int(BPy_AttributeArray *self,
Py_ssize_t index,
PyObject *value_to_assign)
{
Py_ssize_t index_abs = index >= 0 ? index : index + self->data_layer->length;
if (index_abs < 0 || index_abs >= self->data_layer->length) {
PyErr_Format(PyExc_IndexError,
"AttributeArray[index] = value: index %d out of range, size %d",
index,
self->data_layer->length);
return -1;
}
if (!check_attribute_type(self)) {
return -1;
}
if (!set_attribute_by_type(self, int(index), value_to_assign)) {
StringRef type_description = get_attribute_type_description(self);
PyErr_Format(PyExc_ValueError,
"AttributeArray[index] = value: invalid value type, expected a %.200s",
type_description.data());
return -1;
}
return 0;
}
static int BPy_AttributeArray_assign_subscript_slice(BPy_AttributeArray *self,
Py_ssize_t start,
Py_ssize_t stop,
PyObject *values_to_assign)
{
if (!check_attribute_type(self)) {
return -1;
}
if (!PySequence_Check(values_to_assign)) {
StringRef type_description = get_attribute_type_description(self);
PyErr_Format(PyExc_TypeError,
"AttributeArray[:] = value: invalid value type, expected a sequence of %.200s",
type_description.data());
return -1;
}
const Py_ssize_t count = PySequence_Fast_GET_SIZE(values_to_assign);
if (count != stop - start) {
PyErr_Format(PyExc_ValueError,
"AttributeArray[:] = value: size mismatch in assignment (expected %d, got %d)",
stop - start,
count);
return -1;
}
/* Assign sequence to attribute array. */
PyObject **values_fast = PySequence_Fast_ITEMS(values_to_assign);
for (int index = start; index < stop; index++) {
PyObject *item = values_fast[index - start];
if (item) {
if (!set_attribute_by_type(self, index, item)) {
StringRef type_description = get_attribute_type_description(self);
PyErr_Format(PyExc_ValueError,
"AttributeArray[index] = value: invalid value type, expected a %.200s",
type_description.data());
return -1;
}
}
else {
return -1;
}
}
return 0;
}
static int BPy_AttributeArray_assign_subscript(BPy_AttributeArray *self,
PyObject *key,
PyObject *value)
{
if (value == nullptr) {
PyErr_SetString(PyExc_ValueError, "del AttributeArray[key]: not supported");
return -1;
}
if (PyIndex_Check(key)) {
const Py_ssize_t index = PyNumber_AsSsize_t(key, PyExc_IndexError);
if (index == -1 && PyErr_Occurred()) {
return -1;
}
return BPy_AttributeArray_assign_subscript_int(self, index, value);
}
else if (PySlice_Check(key)) {
Py_ssize_t step = 1;
PySliceObject *key_slice = (PySliceObject *)key;
if (key_slice->step != Py_None && !_PyEval_SliceIndex(key, &step)) {
return -1;
}
if (step != 1) {
PyErr_SetString(PyExc_IndexError, "AttributeArray[slice]: slice steps not supported");
return -1;
}
Py_ssize_t start, stop, slicelength;
if (PySlice_GetIndicesEx(key, self->data_layer->length, &start, &stop, &step, &slicelength) <
0)
{
return -1;
}
if (slicelength <= 0) {
return 0;
}
return BPy_AttributeArray_assign_subscript_slice(self, start, stop, value);
}
PyErr_Format(PyExc_IndexError,
"AttributeArray[key] = value: invalid key, must be an int or slice, not %.200s",
Py_TYPE(key)->tp_name);
return -1;
}
static PyObject *BPy_AttributeArray_subscript_int(BPy_AttributeArray *self, Py_ssize_t index)
{
Py_ssize_t index_abs = index >= 0 ? index : index + self->data_layer->length;
if (index_abs < 0 || index_abs >= self->data_layer->length) {
PyErr_Format(PyExc_IndexError,
"AttributeArray[index]: index %d out of range, size %d",
index,
self->data_layer->length);
return nullptr;
}
if (!check_attribute_type(self)) {
return nullptr;
}
return get_attribute_by_type(self, int(index));
}
static PyObject *BPy_AttributeArray_subscript_slice(BPy_AttributeArray *self,
Py_ssize_t start,
Py_ssize_t stop)
{
PyObject *list = PyList_New(stop - start);
for (int i = 0; i < stop - start; i++) {
PyObject *item = BPy_AttributeArray_subscript_int(self, start + i);
PyList_SET_ITEM(list, i, item);
}
return list;
}
static PyObject *BPy_AttributeArray_subscript(BPy_AttributeArray *self, PyObject *key)
{
if (PyIndex_Check(key)) {
const Py_ssize_t keynum = PyNumber_AsSsize_t(key, PyExc_IndexError);
if (keynum == -1 && PyErr_Occurred()) {
return nullptr;
}
return BPy_AttributeArray_subscript_int(self, keynum);
}
else if (PySlice_Check(key)) {
Py_ssize_t step = 1;
PySliceObject *key_slice = (PySliceObject *)key;
if (key_slice->step != Py_None && !_PyEval_SliceIndex(key, &step)) {
return nullptr;
}
if (step != 1) {
PyErr_SetString(PyExc_IndexError, "AttributeArray[slice]: slice steps not supported");
return nullptr;
}
Py_ssize_t start, stop, slicelength;
if (PySlice_GetIndicesEx(key, self->data_layer->length, &start, &stop, &step, &slicelength) <
0)
{
return nullptr;
}
if (slicelength <= 0) {
return PyList_New(0);
}
return BPy_AttributeArray_subscript_slice(self, start, stop);
}
PyErr_Format(PyExc_IndexError,
"AttributeArray[key]: invalid key, must be an int or slice, not %.200s",
Py_TYPE(key)->tp_name);
return nullptr;
}
static Py_ssize_t BPy_AttributeArray_length(BPy_AttributeArray *self)
{
return self->data_layer->length;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name BPy_AttributeArray Foreach get/set
* \{ */
static PyObject *foreach_getset(BPy_AttributeArray *self,
PyObject *args,
const char *function_name,
const bool use_get)
{
PyObject *buffer_obj;
if (!PyArg_ParseTuple(args, "O:foreach_get/set", &buffer_obj)) {
return nullptr;
}
if (!PyObject_CheckBuffer(buffer_obj)) {
PyErr_Format(
PyExc_TypeError,
"%s(array) expected the argument to be a contiguous array (like a Python or numpy array), "
"not a %.200s",
function_name,
Py_TYPE(buffer_obj)->tp_name);
return nullptr;
}
/* Compare buffer with attribute array size and type. */
Py_buffer buffer;
if (PyObject_GetBuffer(buffer_obj, &buffer, PyBUF_ND | PyBUF_FORMAT) == -1) {
PyErr_SetString(PyExc_BufferError, "%s(array): couldn't access the buffer");
return nullptr;
}
if (!check_attribute_type(self)) {
return nullptr;
}
const bool type_is_compatible = check_buffer_format(self, buffer.format);
const int item_size = get_attribute_item_size(self);
const bool size_matches = (buffer.len == int64_t(self->data_layer->length) * item_size);
if (!type_is_compatible || !size_matches) {
PyBuffer_Release(&buffer);
}
if (!type_is_compatible) {
const char expected_buffer_format = get_buffer_format_by_type(self);
PyErr_Format(PyExc_TypeError,
"%s(array): array type mismatch (expected type '%.1s', got '%.1s')",
function_name,
&expected_buffer_format,
buffer.format);
return nullptr;
}
if (!size_matches) {
PyErr_Format(PyExc_ValueError,
"%s(array): array length mismatch (expected %lld, got %lld)",
function_name,
int64_t(self->data_layer->length) * item_size,
buffer.len);
return nullptr;
}
/* Source and destination arrays are contiguous, copy the data at once. */
if (use_get) {
memcpy(buffer.buf, self->data_layer->data, buffer.len);
}
else {
memcpy(self->data_layer->data, buffer.buf, buffer.len);
}
PyBuffer_Release(&buffer);
Py_RETURN_NONE;
}
PyDoc_STRVAR(
/* Wrap. */
BPy_AttributeArray_foreach_get_doc,
".. method:: foreach_get(array)\n"
"\n"
" This is a function to give fast access to the values in an geometry attribute array.\n"
"\n"
" :arg array: An contiguous array of the same size as the attribute array,\n"
" e.g. a Python array.array() or a numpy array.\n"
" :type array: The same type as the attribute array.\n");
static PyObject *BPy_AttributeArray_foreach_get(BPy_AttributeArray *self, PyObject *args)
{
return foreach_getset(self, args, "foreach_get", true);
}
PyDoc_STRVAR(
/* Wrap. */
BPy_AttributeArray_foreach_set_doc,
".. method:: foreach_set(array)\n"
"\n"
" This is a function to give fast access to the values in an geometry attribute array.\n"
"\n"
" :arg array: An contiguous array of the same size as the attribute array,\n"
" e.g. a Python array.array() or a numpy array.\n"
" :type array: The same type as the attribute array.\n");
static PyObject *BPy_AttributeArray_foreach_set(BPy_AttributeArray *self, PyObject *args)
{
return foreach_getset(self, args, "foreach_set", false);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name BPy_AttributeArray Fill
* \{ */
static std::optional<int> get_index_int(PyObject *py_index)
{
const int index = PyLong_AsLong(py_index);
if (index == -1 && PyErr_Occurred()) {
return std::nullopt;
}
return index;
}
PyDoc_STRVAR(
/* Wrap. */
BPy_AttributeArray_fill_doc,
".. method:: fill(value, indices=None)\n"
"\n"
" Fill the geometry attribute array with a value.\n"
"\n"
" :arg value: Value to fill the array with.\n"
" :arg indices: Only fill the given indices (optional argument).\n"
" :type indices: List, array or numpy array of ints.\n");
static PyObject *BPy_AttributeArray_fill(BPy_AttributeArray *self, PyObject *args)
{
/* Get fill value and (optionally) a list of indices. */
PyObject *py_value, *py_indices = nullptr;
if (!PyArg_ParseTuple(args, "O|O:AttributeArray.fill(value, indices)", &py_value, &py_indices)) {
return nullptr;
}
if (!check_attribute_type(self)) {
return nullptr;
}
/* Verify the indices object. */
if (py_indices == Py_None) {
py_indices = nullptr;
}
if (py_indices && !PySequence_Check(py_indices)) {
PyErr_SetString(
PyExc_TypeError,
"AttributeArray.fill[value, indices]: invalid indices type, expected a list of ints");
return nullptr;
}
Py_ssize_t indices_length;
if (py_indices) {
indices_length = PySequence_Size(py_indices);
if (indices_length == 0) {
Py_RETURN_NONE;
}
}
/* Get the index of first value in the array. */
int first_index = 0;
if (py_indices) {
PyObject *py_index = PySequence_GetItem(py_indices, 0);
std::optional<int> maybe_first_index = get_index_int(py_index);
if (!maybe_first_index.has_value()) {
PyErr_Format(
PyExc_IndexError,
"AttributeArray.fill(value, indices): invalid index type (expected an int, got %.200s)",
Py_TYPE(py_index)->tp_name);
Py_DECREF(py_index);
return nullptr;
}
Py_DECREF(py_index);
first_index = maybe_first_index.value();
if (first_index < 0 || first_index >= self->data_layer->length) {
PyErr_Format(PyExc_IndexError,
"AttributeArray.fill(value, indices): index %d out of range, size %d",
first_index,
self->data_layer->length);
return nullptr;
}
}
/* Set the first value. */
if (!set_attribute_by_type(self, first_index, py_value)) {
StringRef type_description = get_attribute_type_description(self);
PyErr_Format(PyExc_ValueError,
"AttributeArray.fill(value, ...): invalid value type, expected a %.200s",
type_description.data());
return nullptr;
}
/* Fill the entire array. */
void *data = self->data_layer->data;
const int item_size = get_attribute_item_size(self);
if (py_indices == nullptr) {
for (int index = 1; index < self->data_layer->length; index++) {
memcpy(static_cast<int8_t *>(data) + index * item_size, data, item_size);
}
}
/* Fill the array using indices. */
else {
for (int i = 1; i < indices_length; i++) {
PyObject *py_index = PySequence_GetItem(py_indices, i);
std::optional<int> maybe_index = get_index_int(py_index);
if (!maybe_index.has_value()) {
PyErr_Format(PyExc_IndexError,
"AttributeArray.fill(value, indices): invalid index type (expected an int, "
"got %.200s)",
Py_TYPE(py_index)->tp_name);
Py_DECREF(py_index);
return nullptr;
}
Py_DECREF(py_index);
const int index = maybe_index.value();
if (index < 0 || index >= self->data_layer->length) {
PyErr_Format(PyExc_IndexError,
"AttributeArray.fill(value, indices): index %d out of range, size %d",
index,
self->data_layer->length);
return nullptr;
}
memcpy(static_cast<int8_t *>(data) + index * item_size,
static_cast<int8_t *>(data) + first_index * item_size,
item_size);
}
}
Py_RETURN_NONE;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name BPy_AttributeArray Miscellaneous
* \{ */
static PyObject *BPy_AttributeArray_repr(BPy_AttributeArray *self)
{
const CustomDataLayer &layer = *self->data_layer;
return PyUnicode_FromFormat("<AttributeArray, '%.200s' at %p>", layer.name, layer.data);
}
static Py_hash_t BPy_AttributeArray_hash(BPy_AttributeArray *self)
{
return _Py_HashPointer(self->data_layer);
}
/** \} */
} // namespace blender::python::attributearray
/* -------------------------------------------------------------------- */
/** \name BPy_AttributeArray Create object
* \{ */
PyObject *BPy_AttributeArray_CreatePyObject(PointerRNA *ptr)
{
BPy_AttributeArray *self = PyObject_New(BPy_AttributeArray, &BPy_AttributeArray_Type);
self->data_layer = static_cast<CustomDataLayer *>(ptr->data);
return (PyObject *)self;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name BPy_AttributeArray Type
* \{ */
using namespace blender::python::attributearray;
static PyMethodDef BPy_AttributeArray_methods[] = {
{"foreach_get",
(PyCFunction)BPy_AttributeArray_foreach_get,
METH_VARARGS,
BPy_AttributeArray_foreach_get_doc},
{"foreach_set",
(PyCFunction)BPy_AttributeArray_foreach_set,
METH_VARARGS,
BPy_AttributeArray_foreach_set_doc},
{"fill", (PyCFunction)BPy_AttributeArray_fill, METH_VARARGS, BPy_AttributeArray_fill_doc},
{nullptr, nullptr, 0, nullptr},
};
static PySequenceMethods BPy_AttributeArray_as_sequence = {
/*sq_length*/ (lenfunc)BPy_AttributeArray_length,
/*sq_concat*/ nullptr,
/*sq_repeat*/ nullptr,
/*sq_item*/ (ssizeargfunc)BPy_AttributeArray_subscript_int,
/*was_sq_slice*/ nullptr,
/*sq_ass_item*/
(ssizeobjargproc)BPy_AttributeArray_assign_subscript_int,
/*was_sq_ass_slice*/ nullptr,
/*sq_contains*/ nullptr,
/*sq_inplace_concat*/ nullptr,
/*sq_inplace_repeat*/ nullptr,
};
static PyMappingMethods BPy_AttributeArray_as_mapping = {
/*mp_length*/ (lenfunc)BPy_AttributeArray_length,
/*mp_subscript*/ (binaryfunc)BPy_AttributeArray_subscript,
/*mp_ass_subscript*/
(objobjargproc)BPy_AttributeArray_assign_subscript,
};
PyTypeObject BPy_AttributeArray_Type = {
/*ob_base*/ PyVarObject_HEAD_INIT(nullptr, 0)
/*tp_name*/ "AttributeArray",
/*tp_basicsize*/ sizeof(BPy_AttributeArray),
/*tp_itemsize*/ 0,
/*tp_dealloc*/ nullptr,
/*tp_vectorcall_offset */ 0,
/*tp_getattr*/ nullptr,
/*tp_setattr*/ nullptr,
/*tp_as_async*/ nullptr,
/*tp_repr*/ (reprfunc)BPy_AttributeArray_repr,
/*tp_as_number*/ nullptr,
/*tp_as_sequence*/ &BPy_AttributeArray_as_sequence,
/*tp_as_mapping*/ &BPy_AttributeArray_as_mapping,
/*tp_hash*/ (hashfunc)BPy_AttributeArray_hash,
/*tp_call*/ nullptr,
/*tp_str*/ nullptr,
/*tp_getattro*/ nullptr,
/*tp_setattro*/ nullptr,
/*tp_as_buffer*/ nullptr,
/*tp_flags*/ Py_TPFLAGS_DEFAULT,
/*tp_doc*/ nullptr,
/*tp_traverse*/ nullptr,
/*tp_clear*/ nullptr,
/*tp_richcompare*/ nullptr,
/*tp_weaklistoffset*/ 0,
/*tp_iter*/ nullptr,
/*tp_iternext*/ nullptr,
/*tp_methods*/ BPy_AttributeArray_methods,
/*tp_members*/ nullptr,
/*tp_getset*/ nullptr,
/*tp_base*/ nullptr,
/*tp_dict*/ nullptr,
/*tp_descr_get*/ nullptr,
/*tp_descr_set*/ nullptr,
/*tp_dictoffset*/ 0,
/*tp_init*/ nullptr,
/*tp_alloc*/ nullptr,
/*tp_new*/ nullptr,
/*tp_free*/ nullptr,
/*tp_is_gc*/ nullptr,
/*tp_bases*/ nullptr,
/*tp_mro*/ nullptr,
/*tp_cache*/ nullptr,
/*tp_subclasses*/ nullptr,
/*tp_weaklist*/ nullptr,
/*tp_del*/ nullptr,
/*tp_version_tag*/ 0,
/*tp_finalize*/ nullptr,
/*tp_vectorcall*/ nullptr,
};
/** \} */
/* -------------------------------------------------------------------- */
/** \name Initialize Types
* \{ */
void AttributeArray_Init_Types()
{
PyType_Ready(&BPy_AttributeArray_Type);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Public Module 'attribute_array.types'
* \{ */
static PyModuleDef AttributeArray_types_module_def = {
/*m_base*/ PyModuleDef_HEAD_INIT,
/*m_name*/ "attribute_array.types",
/*m_doc*/ nullptr,
/*m_size*/ 0,
/*m_methods*/ nullptr,
/*m_slots*/ nullptr,
/*m_traverse*/ nullptr,
/*m_clear*/ nullptr,
/*m_free*/ nullptr,
};
static PyObject *BPyInit_attribute_array_types()
{
PyObject *submodule;
submodule = PyModule_Create(&AttributeArray_types_module_def);
AttributeArray_Init_Types();
PyModule_AddType(submodule, &BPy_AttributeArray_Type);
return submodule;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Public Module 'attribute_array'
* \{ */
PyDoc_STRVAR(
/* Wrap. */
AttributeArray_module_doc,
"This module provides access to geometry attribute arrays.");
static PyModuleDef AttributeArray_module_def = {
/*m_base*/ PyModuleDef_HEAD_INIT,
/*m_name*/ "attribute_array",
/*m_doc*/ AttributeArray_module_doc,
/*m_size*/ 0,
/*m_methods*/ nullptr,
/*m_slots*/ nullptr,
/*m_traverse*/ nullptr,
/*m_clear*/ nullptr,
/*m_free*/ nullptr,
};
PyObject *BPyInit_attribute_array()
{
PyObject *mod;
PyObject *submodule;
PyObject *sys_modules = PyImport_GetModuleDict();
mod = PyModule_Create(&AttributeArray_module_def);
/* attribute_array.types */
PyModule_AddObject(mod, "types", (submodule = BPyInit_attribute_array_types()));
PyDict_SetItem(sys_modules, PyModule_GetNameObject(submodule), submodule);
return mod;
}
/** \} */

View File

@ -0,0 +1,33 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup pygen
*/
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
struct PointerRNA;
struct CustomDataLayer;
extern PyTypeObject BPy_AttributeArray_Type;
typedef struct BPy_AttributeArray {
PyObject_VAR_HEAD
CustomDataLayer *data_layer;
} BPy_AttributeArray;
PyObject *BPy_AttributeArray_CreatePyObject(PointerRNA *ptr);
void AttributeArray_Init_Types(void);
PyObject *BPyInit_attribute_array(void);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,637 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup pygen
*/
#pragma once
#include <functional>
#include <map>
#include <Python.h>
#include "attribute_array_py_api.h"
#include "py_capi_utils.h"
#include "../mathutils/mathutils.h"
#include "DNA_meshdata_types.h"
#include "BKE_customdata.hh"
#include "BLI_color.hh"
#include "BLI_math_matrix_types.hh"
#include "BLI_math_quaternion_types.hh"
#include "BLI_math_vector_types.hh"
namespace blender::python::attributearray {
/* -------------------------------------------------------------------- */
/** \name BPy_AttributeArray Set attribute
* \{ */
static bool set_attribute_bool(void *data, int index, PyObject *py_value)
{
const int value = PyC_Long_AsBool(py_value);
if (value == -1) {
return false;
}
static_cast<bool *>(data)[index] = value;
return true;
}
static bool set_attribute_float(void *data, int index, PyObject *py_value)
{
if (PyLong_Check(py_value)) {
/* Allow int values (like 0), convert them to float. */
const long value = PyLong_AsLong(py_value);
static_cast<float *>(data)[index] = float(value);
return true;
}
if (!PyFloat_Check(py_value)) {
return false;
}
static_cast<float *>(data)[index] = PyFloat_AsDouble(py_value);
return true;
}
static bool set_attribute_int8(void *data, int index, PyObject *py_value)
{
const int value = _PyLong_AsInt(py_value);
if (UNLIKELY(value == -1 && PyErr_Occurred())) {
return false;
}
if (UNLIKELY(value < INT8_MIN || value > INT8_MAX)) {
return false;
}
static_cast<int8_t *>(data)[index] = int8_t(value);
return true;
}
static bool set_attribute_uint8(void *data, int index, PyObject *py_value)
{
const int value = _PyLong_AsInt(py_value);
if (UNLIKELY(value == -1 && PyErr_Occurred())) {
return false;
}
if (UNLIKELY(value < 0 || value > UINT8_MAX)) {
return false;
}
static_cast<uint8_t *>(data)[index] = uint8_t(value);
return true;
}
static bool set_attribute_int32(void *data, int index, PyObject *py_value)
{
if (!PyLong_Check(py_value)) {
return false;
}
const long value = PyLong_AsLong(py_value);
if (UNLIKELY(value < INT32_MIN || value > INT32_MAX)) {
return false;
}
static_cast<int32_t *>(data)[index] = int32_t(value);
return true;
}
static bool set_attribute_int2(void *data, int index, PyObject *py_value)
{
if (PyTuple_Check(py_value)) {
const Py_ssize_t size = PyTuple_GET_SIZE(py_value);
if (size != 2) {
return false;
}
int2 &set_value = static_cast<int2 *>(data)[index];
for (int i = 0; i < 2; i++) {
if (!set_attribute_int32(set_value, i, PyTuple_GET_ITEM(py_value, i))) {
return false;
}
}
return true;
}
if (PyList_Check(py_value)) {
const Py_ssize_t size = PyList_GET_SIZE(py_value);
if (size != 2) {
return false;
}
int2 &set_value = static_cast<int2 *>(data)[index];
for (int i = 0; i < 2; i++) {
if (!set_attribute_int32(set_value, i, PyList_GET_ITEM(py_value, i))) {
return false;
}
}
return true;
}
return false;
}
static bool set_attribute_string(void *data, int index, PyObject *py_value)
{
if (!PyUnicode_Check(py_value)) {
return false;
}
const std::string value = std::string(PyUnicode_AsUTF8(py_value));
if (value.length() > 254) {
return false;
}
strcpy(static_cast<MStringProperty *>(data)[index].s, value.c_str());
static_cast<MStringProperty *>(data)[index].s_len = value.length();
return true;
}
static bool set_attribute_float2(void *data, int index, PyObject *py_value)
{
if (VectorObject_Check(py_value)) {
const VectorObject *value = reinterpret_cast<VectorObject *>(py_value);
if (value->vec_num != 2) {
return false;
}
static_cast<float2 *>(data)[index] = {value->vec[0], value->vec[1]};
return true;
}
if (PyTuple_Check(py_value)) {
const Py_ssize_t size = PyTuple_GET_SIZE(py_value);
if (size != 2) {
return false;
}
float2 &set_value = static_cast<float2 *>(data)[index];
for (int i = 0; i < 2; i++) {
if (!set_attribute_float(set_value, i, PyTuple_GET_ITEM(py_value, i))) {
return false;
}
}
return true;
}
if (PyList_Check(py_value)) {
const Py_ssize_t size = PyList_GET_SIZE(py_value);
if (size != 2) {
return false;
}
float2 &set_value = static_cast<float2 *>(data)[index];
for (int i = 0; i < 2; i++) {
if (!set_attribute_float(set_value, i, PyList_GET_ITEM(py_value, i))) {
return false;
}
}
return true;
}
return false;
}
static bool set_attribute_float3(void *data, int index, PyObject *py_value)
{
if (VectorObject_Check(py_value)) {
const VectorObject *value = reinterpret_cast<VectorObject *>(py_value);
if (value->vec_num != 3) {
return false;
}
static_cast<float3 *>(data)[index] = {value->vec[0], value->vec[1], value->vec[2]};
return true;
}
if (PyTuple_Check(py_value)) {
const Py_ssize_t size = PyTuple_GET_SIZE(py_value);
if (size != 3) {
return false;
}
float3 &set_value = static_cast<float3 *>(data)[index];
for (int i = 0; i < 3; i++) {
if (!set_attribute_float(set_value, i, PyTuple_GET_ITEM(py_value, i))) {
return false;
}
}
return true;
}
if (PyList_Check(py_value)) {
const Py_ssize_t size = PyList_GET_SIZE(py_value);
if (size != 3) {
return false;
}
float3 &set_value = static_cast<float3 *>(data)[index];
for (int i = 0; i < 3; i++) {
if (!set_attribute_float(set_value, i, PyList_GET_ITEM(py_value, i))) {
return false;
}
}
return true;
}
return false;
}
static bool set_attribute_quaternion(void *data, int index, PyObject *py_value)
{
if (QuaternionObject_Check(py_value)) {
const QuaternionObject *value = reinterpret_cast<QuaternionObject *>(py_value);
static_cast<math::Quaternion *>(data)[index] = {
value->quat[0], value->quat[1], value->quat[2], value->quat[3]};
return true;
}
if (PyTuple_Check(py_value)) {
const Py_ssize_t size = PyTuple_GET_SIZE(py_value);
if (size != 4) {
return false;
}
float4 &set_value = static_cast<float4 *>(data)[index];
for (int i = 0; i < 4; i++) {
if (!set_attribute_float(set_value, i, PyTuple_GET_ITEM(py_value, i))) {
return false;
}
}
return true;
}
if (PyList_Check(py_value)) {
const Py_ssize_t size = PyList_GET_SIZE(py_value);
if (size != 4) {
return false;
}
float4 &set_value = static_cast<float4 *>(data)[index];
for (int i = 0; i < 4; i++) {
if (!set_attribute_float(set_value, i, PyList_GET_ITEM(py_value, i))) {
return false;
}
}
return true;
}
return false;
}
static bool set_attribute_float4x4(void *data, int index, PyObject *py_value)
{
if (!MatrixObject_Check(py_value)) {
return false;
}
MatrixObject &value = *reinterpret_cast<MatrixObject *>(py_value);
if (value.col_num != 4 || value.row_num != 4) {
return false;
}
std::copy_n(value.matrix, 16, static_cast<float4x4 *>(data)[index].base_ptr());
return true;
}
static bool set_attribute_color(void *data, int index, PyObject *py_value)
{
if (PyTuple_Check(py_value)) {
const Py_ssize_t size = PyTuple_GET_SIZE(py_value);
if (size != 4) {
return false;
}
ColorGeometry4f &set_value = static_cast<ColorGeometry4f *>(data)[index];
for (int i = 0; i < 4; i++) {
if (!set_attribute_float(set_value, i, PyTuple_GET_ITEM(py_value, i))) {
return false;
}
}
return true;
}
if (PyList_Check(py_value)) {
const Py_ssize_t size = PyList_GET_SIZE(py_value);
if (size != 4) {
return false;
}
ColorGeometry4f &set_value = static_cast<ColorGeometry4f *>(data)[index];
for (int i = 0; i < 4; i++) {
if (!set_attribute_float(set_value, i, PyList_GET_ITEM(py_value, i))) {
return false;
}
}
return true;
}
return false;
}
static bool set_attribute_byte_color(void *data, int index, PyObject *py_value)
{
if (PyTuple_Check(py_value)) {
const Py_ssize_t size = PyTuple_GET_SIZE(py_value);
if (size != 4) {
return false;
}
ColorGeometry4b &set_value = static_cast<ColorGeometry4b *>(data)[index];
for (int i = 0; i < 4; i++) {
if (!set_attribute_uint8(set_value, i, PyTuple_GET_ITEM(py_value, i))) {
return false;
}
}
return true;
}
if (PyList_Check(py_value)) {
const Py_ssize_t size = PyList_GET_SIZE(py_value);
if (size != 4) {
return false;
}
ColorGeometry4b &set_value = static_cast<ColorGeometry4b *>(data)[index];
for (int i = 0; i < 4; i++) {
if (!set_attribute_uint8(set_value, i, PyList_GET_ITEM(py_value, i))) {
return false;
}
}
return true;
}
return false;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name BPy_AttributeArray Get attribute
* \{ */
PyObject *get_attribute_bool(void *data, int index)
{
return PyBool_FromLong(static_cast<bool *>(data)[index]);
}
PyObject *get_attribute_float(void *data, int index)
{
return PyFloat_FromDouble(static_cast<float *>(data)[index]);
}
PyObject *get_attribute_int8(void *data, int index)
{
return PyLong_FromLong(static_cast<int8_t *>(data)[index]);
}
PyObject *get_attribute_int32(void *data, int index)
{
return PyLong_FromLong(static_cast<int32_t *>(data)[index]);
}
PyObject *get_attribute_int2(void *data, int index)
{
PyObject *result = PyTuple_New(2);
for (int i = 0; i < 2; i++) {
PyTuple_SET_ITEM(result, i, PyLong_FromLong((static_cast<int2 *>(data)[index])[i]));
}
return result;
}
PyObject *get_attribute_string(void *data, int index)
{
return PyUnicode_FromStringAndSize(static_cast<MStringProperty *>(data)[index].s,
static_cast<MStringProperty *>(data)[index].s_len);
}
PyObject *get_attribute_float2(void *data, int index)
{
return Vector_CreatePyObject_wrap(static_cast<float2 *>(data)[index], 2, nullptr);
}
PyObject *get_attribute_float3(void *data, int index)
{
return Vector_CreatePyObject_wrap(static_cast<float3 *>(data)[index], 3, nullptr);
}
PyObject *get_attribute_float4x4(void *data, int index)
{
return Matrix_CreatePyObject_wrap(
static_cast<float *>((static_cast<float4x4 *>(data)[index]).base_ptr()), 4, 4, nullptr);
}
PyObject *get_attribute_quaternion(void *data, int index)
{
return Quaternion_CreatePyObject_wrap(static_cast<float4 *>(data)[index], nullptr);
}
PyObject *get_attribute_color(void *data, int index)
{
PyObject *result = PyTuple_New(4);
for (int i = 0; i < 4; i++) {
PyTuple_SET_ITEM(
result, i, PyFloat_FromDouble((static_cast<ColorGeometry4f *>(data)[index])[i]));
}
return result;
}
PyObject *get_attribute_byte_color(void *data, int index)
{
PyObject *result = PyTuple_New(4);
for (int i = 0; i < 4; i++) {
PyTuple_SET_ITEM(result, i, PyLong_FromLong((static_cast<ColorGeometry4b *>(data)[index])[i]));
}
return result;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name BPy_AttributeArray Map attribute types to functions
* \{ */
bool set_attribute_by_type(BPy_AttributeArray *self, int index, PyObject *value)
{
void *data = self->data_layer->data;
switch (self->data_layer->type) {
case CD_PROP_BOOL:
return set_attribute_bool(data, index, value);
SietseB marked this conversation as resolved Outdated

Use simpler types here: function pointers and StringRef will do the trick and will be faster. That said, I'm not sure about passing everything through a function pointer. The indirection seems unnecessarily complex. Why not just use a switch statement?

Use simpler types here: function pointers and `StringRef` will do the trick and will be faster. That said, I'm not sure about passing everything through a function pointer. The indirection seems unnecessarily complex. Why not just use a switch statement?
case CD_PROP_FLOAT:
return set_attribute_float(data, index, value);
case CD_PROP_INT8:
return set_attribute_int8(data, index, value);
SietseB marked this conversation as resolved Outdated

Use Map in Blender code rather than the std library types

Use `Map` in Blender code rather than the std library types
case CD_PROP_INT32:
return set_attribute_int32(data, index, value);
case CD_PROP_INT32_2D:
return set_attribute_int2(data, index, value);
case CD_PROP_STRING:
return set_attribute_string(data, index, value);
case CD_PROP_FLOAT2:
return set_attribute_float2(data, index, value);
case CD_PROP_FLOAT3:
return set_attribute_float3(data, index, value);
case CD_PROP_FLOAT4X4:
return set_attribute_float4x4(data, index, value);
case CD_PROP_QUATERNION:
return set_attribute_quaternion(data, index, value);
case CD_PROP_COLOR:
return set_attribute_color(data, index, value);
case CD_PROP_BYTE_COLOR:
return set_attribute_byte_color(data, index, value);
}
return false;
}
PyObject *get_attribute_by_type(const BPy_AttributeArray *self, int index)
{
void *data = self->data_layer->data;
switch (self->data_layer->type) {
case CD_PROP_BOOL:
return get_attribute_bool(data, index);
case CD_PROP_FLOAT:
return get_attribute_float(data, index);
case CD_PROP_INT8:
return get_attribute_int8(data, index);
case CD_PROP_INT32:
return get_attribute_int32(data, index);
case CD_PROP_INT32_2D:
return get_attribute_int2(data, index);
case CD_PROP_STRING:
return get_attribute_string(data, index);
case CD_PROP_FLOAT2:
return get_attribute_float2(data, index);
case CD_PROP_FLOAT3:
return get_attribute_float3(data, index);
case CD_PROP_FLOAT4X4:
return get_attribute_float4x4(data, index);
case CD_PROP_QUATERNION:
return get_attribute_quaternion(data, index);
case CD_PROP_COLOR:
return get_attribute_color(data, index);
case CD_PROP_BYTE_COLOR:
return get_attribute_byte_color(data, index);
}
return nullptr;
}
int get_attribute_item_size(BPy_AttributeArray *self)
{
switch (self->data_layer->type) {
case CD_PROP_BOOL:
return sizeof(bool);
case CD_PROP_FLOAT:
return sizeof(float);
case CD_PROP_INT8:
return sizeof(int8_t);
case CD_PROP_INT32:
return sizeof(int32_t);
case CD_PROP_INT32_2D:
return sizeof(int2);
case CD_PROP_STRING:
return sizeof(MStringProperty);
case CD_PROP_FLOAT2:
return sizeof(float2);
case CD_PROP_FLOAT3:
return sizeof(float3);
case CD_PROP_FLOAT4X4:
return sizeof(float4x4);
case CD_PROP_QUATERNION:
return sizeof(math::Quaternion);
case CD_PROP_COLOR:
return sizeof(ColorGeometry4f);
case CD_PROP_BYTE_COLOR:
return sizeof(ColorGeometry4b);
}
return 0;
}
StringRef get_attribute_type_description(const BPy_AttributeArray *self)
{
switch (self->data_layer->type) {
case CD_PROP_BOOL:
return "bool value True/False or 0/1";
case CD_PROP_FLOAT:
return "float";
case CD_PROP_INT8:
return "int8 between -128 and 127";
case CD_PROP_INT32:
return "int";
case CD_PROP_INT32_2D:
return "tuple (x, y) or list [x, y] of ints";
case CD_PROP_STRING:
return "string with a maximum of 254 characters";
case CD_PROP_FLOAT2:
return "mathutils.Vector((x, y)), tuple (x, y) or list [x, y]";
case CD_PROP_FLOAT3:
return "mathutils.Vector((x, y, z)), tuple (x, y, z) or list [x, y, z]";
case CD_PROP_FLOAT4X4:
return "mathutils.Matrix() with 4x4 dimensions";
case CD_PROP_QUATERNION:
return "mathutils.Quaternion((w, x, y, z)), tuple (w, x, y, z) or list [w, x, y, z]";
case CD_PROP_COLOR:
return "tuple (r, g, b, a) or list [r, g, b, a] of floats";
case CD_PROP_BYTE_COLOR:
return "tuple (r, g, b, a) or list [r, g, b, a] of int in the range 0-255";
}
return "";
}
char get_buffer_format_by_type(BPy_AttributeArray *self)
{
switch (self->data_layer->type) {
case CD_PROP_BOOL:
return '?';
case CD_PROP_FLOAT:
return 'f';
case CD_PROP_INT8:
return 'b';
case CD_PROP_INT32:
return 'i';
case CD_PROP_INT32_2D:
return 'i';
case CD_PROP_STRING:
return 'B';
case CD_PROP_FLOAT2:
return 'f';
case CD_PROP_FLOAT3:
return 'f';
case CD_PROP_FLOAT4X4:
return 'f';
case CD_PROP_QUATERNION:
return 'f';
case CD_PROP_COLOR:
return 'f';
case CD_PROP_BYTE_COLOR:
return 'B';
}
return ' ';
}
bool check_buffer_format(const BPy_AttributeArray *self, const char *buffer_format)
{
const char f = buffer_format ? *buffer_format : 'B';
switch (self->data_layer->type) {
case CD_PROP_BOOL:
return f == '?';
case CD_PROP_FLOAT:
return f == 'f';
case CD_PROP_INT8:
return f == 'b';
case CD_PROP_INT32:
return f == 'i' || f == 'l';
case CD_PROP_INT32_2D:
return f == 'i' || f == 'l';
case CD_PROP_STRING:
return f == 'B';
case CD_PROP_FLOAT2:
return f == 'f';
case CD_PROP_FLOAT3:
return f == 'f';
case CD_PROP_FLOAT4X4:
return f == 'f';
case CD_PROP_QUATERNION:
return f == 'f';
case CD_PROP_COLOR:
return f == 'f';
case CD_PROP_BYTE_COLOR:
return f == 'B';
}
return false;
}
bool check_attribute_type(const BPy_AttributeArray *self)
{
switch (self->data_layer->type) {
case CD_PROP_BOOL:
case CD_PROP_FLOAT:
case CD_PROP_INT8:
case CD_PROP_INT32:
case CD_PROP_INT32_2D:
case CD_PROP_STRING:
case CD_PROP_FLOAT2:
case CD_PROP_FLOAT3:
case CD_PROP_FLOAT4X4:
case CD_PROP_QUATERNION:
case CD_PROP_COLOR:
case CD_PROP_BYTE_COLOR:
return true;
}
PyErr_Format(
PyExc_TypeError,
"AttributeArray[index]: unexpected error, data type for attribute '%.200s' not found",
self->data_layer->name);
return false;
}
} // namespace blender::python::attributearray

View File

@ -51,6 +51,7 @@
#include "../generic/python_utildefines.h"
/* external util modules */
#include "../generic/attribute_array_py_api.h"
#include "../generic/idprop_py_api.h"
#include "../generic/idprop_py_ui_api.h"
#include "bpy_msgbus.h"
@ -728,6 +729,7 @@ void BPy_init_modules(bContext *C)
/* stand alone utility modules not related to blender directly */
IDProp_Init_Types(); /* not actually a submodule, just types */
IDPropertyUIData_Init_Types();
AttributeArray_Init_Types(); /* not actually a submodule, just types */
#ifdef WITH_FREESTYLE
Freestyle_Init();
#endif

View File

@ -63,6 +63,7 @@
/* `inittab` initialization functions. */
#include "../bmesh/bmesh_py_api.h"
#include "../generic/attribute_array_py_api.h"
#include "../generic/bgl.h"
#include "../generic/bl_math_py_api.h"
#include "../generic/blf_py_api.h"
@ -300,6 +301,7 @@ static _inittab bpy_internal_modules[] = {
#endif
{"gpu", BPyInit_gpu},
{"idprop", BPyInit_idprop},
{"attribute_array", BPyInit_attribute_array},
#ifdef WITH_HYDRA
{"_bpy_hydra", BPyInit_hydra},
#endif

View File

@ -62,6 +62,7 @@
#include "DEG_depsgraph_query.hh"
#include "../generic/attribute_array_py_api.h"
#include "../generic/idprop_py_api.h" /* For IDprop lookups. */
#include "../generic/idprop_py_ui_api.h"
#include "../generic/py_capi_rna.h"
@ -1454,7 +1455,12 @@ PyObject *pyrna_prop_to_py(PointerRNA *ptr, PropertyRNA *prop)
PointerRNA newptr;
newptr = RNA_property_pointer_get(ptr, prop);
if (newptr.data) {
ret = pyrna_struct_CreatePyObject(&newptr);
if (RNA_struct_is_attribute_array(newptr.type)) {
ret = BPy_AttributeArray_CreatePyObject(&newptr);
}
else {
ret = pyrna_struct_CreatePyObject(&newptr);
}
}
else {
ret = Py_None;