/* SPDX-FileCopyrightText: 2025 Blender Authors * * SPDX-License-Identifier: GPL-2.0-or-later */ /** \file * \ingroup pythonintern * * This file contains the `bpy.types.GeometrySet` Python API which is a wrapper for the internal * `GeometrySet` type. * * It's not implemented as RNA type because a `GeometrySet` is standalone (i.e. is not necessarily * owned by anything else in Blender like an ID), is wrapping a DNA type and is itself a * non-trivial owner of other data (like sub-geometries). */ #include #include "BKE_duplilist.hh" #include "BKE_geometry_set.hh" #include "BKE_geometry_set_instances.hh" #include "BKE_idtype.hh" #include "BKE_instances.hh" #include "BKE_lib_id.hh" #include "BKE_mesh_wrapper.hh" #include "BKE_pointcloud.hh" #include "DEG_depsgraph_query.hh" #include "DNA_ID.h" #include "DNA_collection_types.h" #include "DNA_mesh_types.h" #include "DNA_object_types.h" #include "DNA_pointcloud_types.h" #include "RNA_enum_types.hh" #include "RNA_prototypes.hh" #include "bpy_geometry_set.hh" #include "bpy_rna.hh" #include "../generic/py_capi_utils.hh" using blender::bke::GeometrySet; extern PyTypeObject bpy_geometry_set_Type; struct BPy_GeometrySet { PyObject_HEAD GeometrySet geometry; PointCloud *instances_pointcloud; }; static BPy_GeometrySet *python_object_from_geometry_set(GeometrySet geometry = {}) { BPy_GeometrySet *self = reinterpret_cast( bpy_geometry_set_Type.tp_alloc(&bpy_geometry_set_Type, 0)); if (self == nullptr) { return nullptr; } new (&self->geometry) GeometrySet(std::move(geometry)); self->instances_pointcloud = nullptr; /* We can't safely give access to shared geometries via the Python API currently, because * constness can't be enforced. Therefore, ensure that this Python object has its own copy of * each data-block. Note that attributes may still be shared with other data in Blender. */ self->geometry.ensure_no_shared_components(); return self; } static PyObject *BPy_GeometrySet_new(PyTypeObject * /*type*/, PyObject *args, PyObject *kwds) { static const char *kwlist[] = {nullptr}; if (!PyArg_ParseTupleAndKeywords(args, kwds, "", const_cast(kwlist))) { return nullptr; } return reinterpret_cast(python_object_from_geometry_set()); } static void BPy_GeometrySet_dealloc(BPy_GeometrySet *self) { std::destroy_at(&self->geometry); if (self->instances_pointcloud) { BKE_id_free(nullptr, self->instances_pointcloud); } Py_TYPE(self)->tp_free(reinterpret_cast(self)); } PyDoc_STRVAR( /* Wrap. */ bpy_geometry_set_from_evaluated_object_doc, ".. staticmethod:: from_evaluated_object(evaluated_object)\n" "\n" " Create a geometry set from the evaluated geometry of an evaluated object.\n" " Typically, it's more convenient to use :func:`bpy.types.Object.evaluated_geometry`.\n" "\n" " :arg evaluated_object: The evaluated object to create a geometry set from.\n" " :type evaluated_object: bpy.types.Object\n"); static BPy_GeometrySet *BPy_GeometrySet_static_from_evaluated_object(PyObject * /*self*/, PyObject *args, PyObject *kwds) { using namespace blender; static const char *kwlist[] = {"evaluated_object", nullptr}; PyObject *py_evaluated_object; if (!PyArg_ParseTupleAndKeywords( args, kwds, "O", const_cast(kwlist), &py_evaluated_object)) { return nullptr; } ID *evaluated_object_id = nullptr; if (!pyrna_id_FromPyObject(py_evaluated_object, &evaluated_object_id)) { PyErr_Format( PyExc_TypeError, "Expected an Object, not %.200s", Py_TYPE(py_evaluated_object)->tp_name); return nullptr; } if (GS(evaluated_object_id->name) != ID_OB) { PyErr_Format(PyExc_TypeError, "Expected an Object, not %.200s", BKE_idtype_idcode_to_name(GS(evaluated_object_id->name))); return nullptr; } Object *evaluated_object = reinterpret_cast(evaluated_object_id); if (!DEG_is_evaluated(evaluated_object)) { PyErr_SetString(PyExc_TypeError, "Expected an evaluated object"); return nullptr; } const bool is_instance_collection = evaluated_object->type == OB_EMPTY && evaluated_object->instance_collection; const bool valid_object_type = OB_TYPE_IS_GEOMETRY(evaluated_object->type) || is_instance_collection; if (!valid_object_type) { const char *ob_type_name = ""; RNA_enum_name_from_value(rna_enum_object_type_items, evaluated_object->type, &ob_type_name); PyErr_Format(PyExc_TypeError, "Expected a geometry object, not %.200s", ob_type_name); return nullptr; } if (!DEG_object_geometry_is_evaluated(*evaluated_object)) { PyErr_SetString(PyExc_TypeError, "Object geometry is not yet evaluated, is the depsgraph evaluated?"); return nullptr; } Depsgraph *depsgraph = DEG_get_depsgraph_by_id(*evaluated_object_id); if (!depsgraph) { PyErr_SetString(PyExc_TypeError, "Object is not owned by a depsgraph"); return nullptr; } Scene *scene = DEG_get_input_scene(depsgraph); GeometrySet geometry; if (is_instance_collection) { bke::Instances *instances = new bke::Instances(); instances->add_new_reference(bke::InstanceReference{*evaluated_object->instance_collection}); instances->add_instance(0, float4x4::identity()); geometry.replace_instances(instances); } else { bke::Instances instances = object_duplilist_legacy_instances( *depsgraph, *scene, *evaluated_object); geometry = bke::object_get_evaluated_geometry_set(*evaluated_object, false); if (instances.instances_num() > 0) { geometry.replace_instances(new bke::Instances(std::move(instances))); } } BPy_GeometrySet *self = python_object_from_geometry_set(std::move(geometry)); return self; } static PyObject *BPy_GeometrySet_repr(BPy_GeometrySet *self) { std::stringstream ss; ss << self->geometry; std::string str = ss.str(); return PyC_UnicodeFromStdStr(str); } PyDoc_STRVAR( /* Wrap. */ bpy_geometry_set_get_instances_pointcloud_doc, ".. method:: instances_pointcloud()\n" "\n" " Get a pointcloud that encodes information about the instances of the geometry.\n" " The returned pointcloud should not be modified.\n" " There is a point per instance and per-instance data is stored in point attributes.\n" " The local transforms are stored in the ``instance_transform`` attribute.\n" " The data instanced by each point is referenced by the ``.reference_index`` attribute,\n" " indexing into the list returned by :func:`bpy.types.GeometrySet.instance_references`.\n" "\n" " :rtype: bpy.types.PointCloud\n"); static PyObject *BPy_GeometrySet_get_instances_pointcloud(BPy_GeometrySet *self) { using namespace blender; const bke::Instances *instances = self->geometry.get_instances(); if (!instances) { Py_RETURN_NONE; } if (self->instances_pointcloud == nullptr) { const int instances_num = instances->instances_num(); PointCloud *pointcloud = BKE_pointcloud_new_nomain(instances_num); bke::gather_attributes(instances->attributes(), bke::AttrDomain::Instance, bke::AttrDomain::Point, {}, IndexMask(instances_num), pointcloud->attributes_for_write()); self->instances_pointcloud = pointcloud; } return pyrna_id_CreatePyObject(&self->instances_pointcloud->id); } PyDoc_STRVAR( /* Wrap. */ bpy_geometry_set_get_instance_references_doc, ".. method:: instance_references()\n" "\n" " This returns a list of geometries that is indexed by the ``.reference_index``\n" " attribute of the pointcloud returned by \n" " :func:`bpy.types.GeometrySet.instances_pointcloud`.\n" " It may contain other geometry sets, objects, collections and None values.\n" "\n" " :rtype: list[None | bpy.types.Object | bpy.types.Collection | bpy.types.GeometrySet]\n"); static PyObject *BPy_GeometrySet_get_instance_references(BPy_GeometrySet *self) { using namespace blender; const bke::Instances *instances = self->geometry.get_instances(); if (!instances) { return PyList_New(0); } const Span references = instances->references(); PyObject *py_references = PyList_New(references.size()); for (const int i : references.index_range()) { const bke::InstanceReference &reference = references[i]; switch (reference.type()) { case bke::InstanceReference::Type::None: { PyList_SET_ITEM(py_references, i, Py_NewRef(Py_None)); break; } case bke::InstanceReference::Type::Object: { Object &object = reference.object(); PyList_SET_ITEM(py_references, i, pyrna_id_CreatePyObject(&object.id)); break; } case bke::InstanceReference::Type::Collection: { Collection &collection = reference.collection(); PyList_SET_ITEM(py_references, i, pyrna_id_CreatePyObject(&collection.id)); break; } case bke::InstanceReference::Type::GeometrySet: { const bke::GeometrySet &geometry_set = reference.geometry_set(); PyList_SET_ITEM(py_references, i, python_object_from_geometry_set(geometry_set)); break; } } } return py_references; } PyDoc_STRVAR( /* Wrap. */ bpy_geometry_set_name_doc, "The name of the geometry set. It can be used for debugging purposes and is not unique.\n" "\n" ":type: str\n"); static PyObject *BPy_GeometrySet_get_name(BPy_GeometrySet *self, void * /*closure*/) { return PyC_UnicodeFromStdStr(self->geometry.name); } static int BPy_GeometrySet_set_name(BPy_GeometrySet *self, PyObject *value, void * /*closure*/) { if (!PyUnicode_Check(value)) { PyErr_SetString(PyExc_TypeError, "expected a string"); return -1; } const char *name = PyUnicode_AsUTF8(value); self->geometry.name = name; return 0; } PyDoc_STRVAR( /* Wrap. */ bpy_geometry_set_mesh_doc, "The mesh data-block in the geometry set.\n" "\n" ":type: :class:`bpy.types.Mesh`\n"); static PyObject *BPy_GeometrySet_get_mesh(BPy_GeometrySet *self, void * /*closure*/) { Mesh *base_mesh = self->geometry.get_mesh_for_write(); if (!base_mesh) { Py_RETURN_NONE; } Mesh *mesh = BKE_mesh_wrapper_ensure_subdivision(base_mesh); return pyrna_id_CreatePyObject(reinterpret_cast(mesh)); } PyDoc_STRVAR( /* Wrap. */ bpy_geometry_set_mesh_base_doc, "The mesh data-block in the geometry set without final subdivision.\n" "\n" ":type: :class:`bpy.types.Mesh`\n"); static PyObject *BPy_GeometrySet_get_mesh_base(BPy_GeometrySet *self, void * /*closure*/) { Mesh *base_mesh = self->geometry.get_mesh_for_write(); return pyrna_id_CreatePyObject(reinterpret_cast(base_mesh)); } PyDoc_STRVAR( /* Wrap. */ bpy_geometry_set_pointcloud_doc, "The point cloud data-block in the geometry set.\n" "\n" ":type: :class:`bpy.types.PointCloud`\n"); static PyObject *BPy_GeometrySet_get_pointcloud(BPy_GeometrySet *self, void * /*closure*/) { return pyrna_id_CreatePyObject( reinterpret_cast(self->geometry.get_pointcloud_for_write())); } PyDoc_STRVAR( /* Wrap. */ bpy_geometry_set_curves_doc, "The curves data-block in the geometry set.\n" "\n" ":type: :class:`bpy.types.Curves`\n"); static PyObject *BPy_GeometrySet_get_curves(BPy_GeometrySet *self, void * /*closure*/) { return pyrna_id_CreatePyObject(reinterpret_cast(self->geometry.get_curves_for_write())); } PyDoc_STRVAR( /* Wrap. */ bpy_geometry_set_volume_doc, "The volume data-block in the geometry set.\n" "\n" ":type: :class:`bpy.types.Volume`\n"); static PyObject *BPy_GeometrySet_get_volume(BPy_GeometrySet *self, void * /*closure*/) { return pyrna_id_CreatePyObject(reinterpret_cast(self->geometry.get_volume_for_write())); } PyDoc_STRVAR( /* Wrap. */ bpy_geometry_set_grease_pencil_doc, "The Grease Pencil data-block in the geometry set.\n" "\n" ":type: :class:`bpy.types.GreasePencil`\n"); static PyObject *BPy_GeometrySet_get_grease_pencil(BPy_GeometrySet *self, void * /*closure*/) { return pyrna_id_CreatePyObject( reinterpret_cast(self->geometry.get_grease_pencil_for_write())); } static PyGetSetDef BPy_GeometrySet_getseters[] = { { "name", reinterpret_cast(BPy_GeometrySet_get_name), reinterpret_cast(BPy_GeometrySet_set_name), bpy_geometry_set_name_doc, nullptr, }, { "mesh", reinterpret_cast(BPy_GeometrySet_get_mesh), nullptr, bpy_geometry_set_mesh_doc, nullptr, }, { "mesh_base", reinterpret_cast(BPy_GeometrySet_get_mesh_base), nullptr, bpy_geometry_set_mesh_base_doc, nullptr, }, { "pointcloud", reinterpret_cast(BPy_GeometrySet_get_pointcloud), nullptr, bpy_geometry_set_pointcloud_doc, nullptr, }, { "curves", reinterpret_cast(BPy_GeometrySet_get_curves), nullptr, bpy_geometry_set_curves_doc, nullptr, }, { "volume", reinterpret_cast(BPy_GeometrySet_get_volume), nullptr, bpy_geometry_set_volume_doc, nullptr, }, { "grease_pencil", reinterpret_cast(BPy_GeometrySet_get_grease_pencil), nullptr, bpy_geometry_set_grease_pencil_doc, nullptr, }, {nullptr}, }; #ifdef __GNUC__ # ifdef __clang__ # pragma clang diagnostic push # pragma clang diagnostic ignored "-Wcast-function-type" # else # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wcast-function-type" # endif #endif static PyMethodDef BPy_GeometrySet_methods[] = { {"from_evaluated_object", reinterpret_cast(BPy_GeometrySet_static_from_evaluated_object), METH_VARARGS | METH_KEYWORDS | METH_STATIC, bpy_geometry_set_from_evaluated_object_doc}, {"instances_pointcloud", reinterpret_cast(BPy_GeometrySet_get_instances_pointcloud), METH_NOARGS, bpy_geometry_set_get_instances_pointcloud_doc}, {"instance_references", reinterpret_cast(BPy_GeometrySet_get_instance_references), METH_NOARGS, bpy_geometry_set_get_instance_references_doc}, {nullptr, nullptr, 0, nullptr}, }; #ifdef __GNUC__ # ifdef __clang__ # pragma clang diagnostic pop # else # pragma GCC diagnostic pop # endif #endif PyDoc_STRVAR( /* Wrap. */ bpy_geometry_set_doc, "Stores potentially multiple geometry components of different types.\n" "For example, it might contain a mesh, curves and nested instances.\n"); PyTypeObject bpy_geometry_set_Type = { /*ob_base*/ PyVarObject_HEAD_INIT(nullptr, 0) /*tp_name*/ "GeometrySet", /*tp_basicsize*/ sizeof(BPy_GeometrySet), /*tp_itemsize*/ 0, /*tp_dealloc*/ reinterpret_cast(BPy_GeometrySet_dealloc), /*tp_vectorcall_offset*/ 0, /*tp_getattr*/ nullptr, /*tp_setattr*/ nullptr, /*tp_as_async*/ nullptr, /*tp_repr*/ reinterpret_cast(BPy_GeometrySet_repr), /*tp_as_number*/ nullptr, /*tp_as_sequence*/ nullptr, /*tp_as_mapping*/ nullptr, /*tp_hash*/ nullptr, /*tp_call*/ nullptr, /*tp_str*/ nullptr, /*tp_getattro*/ nullptr, /*tp_setattro*/ nullptr, /*tp_as_buffer*/ nullptr, /*tp_flags*/ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_doc*/ bpy_geometry_set_doc, /*tp_traverse*/ nullptr, /*tp_clear*/ nullptr, /*tp_richcompare*/ nullptr, /*tp_weaklistoffset*/ 0, /*tp_iter*/ nullptr, /*tp_iternext*/ nullptr, /*tp_methods*/ BPy_GeometrySet_methods, /*tp_members*/ nullptr, /*tp_getset*/ BPy_GeometrySet_getseters, /*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*/ BPy_GeometrySet_new, }; PyObject *BPyInit_geometry_set_type() { if (PyType_Ready(&bpy_geometry_set_Type) < 0) { return nullptr; } return reinterpret_cast(&bpy_geometry_set_Type); }