diff --git a/panda/src/collide/CMakeLists.txt b/panda/src/collide/CMakeLists.txt index 296a40eeed..8dad7fe29a 100644 --- a/panda/src/collide/CMakeLists.txt +++ b/panda/src/collide/CMakeLists.txt @@ -74,6 +74,8 @@ set(P3COLLIDE_IGATEEXT collisionHandlerPhysical_ext.h collisionHandlerQueue_ext.cxx collisionHandlerQueue_ext.h + collisionNode_ext.cxx + collisionNode_ext.h collisionPolygon_ext.cxx collisionPolygon_ext.h collisionTraverser_ext.cxx diff --git a/panda/src/collide/collisionNode.I b/panda/src/collide/collisionNode.I index 51fe8002fb..f429fccfb1 100644 --- a/panda/src/collide/collisionNode.I +++ b/panda/src/collide/collisionNode.I @@ -166,3 +166,24 @@ INLINE CollideMask CollisionNode:: get_default_collide_mask() { return default_collision_node_collide_mask; } + +/** + * Returns the custom pointer set via set_owner(). + */ +INLINE void *CollisionNode:: +get_owner() const { + return _owner; +} + +/** + * Sets a custom pointer, together with an optional callback that will be + * called when the node is deleted. + * + * The owner or the callback will not be copied along with the CollisionNode. + */ +INLINE void CollisionNode:: +set_owner(void *owner, OwnerCallback *callback) { + clear_owner(); + _owner = owner; + _owner_callback = callback; +} diff --git a/panda/src/collide/collisionNode.cxx b/panda/src/collide/collisionNode.cxx index 2df893ef18..60d28960dd 100644 --- a/panda/src/collide/collisionNode.cxx +++ b/panda/src/collide/collisionNode.cxx @@ -40,7 +40,9 @@ CollisionNode:: CollisionNode(const std::string &name) : PandaNode(name), _from_collide_mask(get_default_collide_mask()), - _collider_sort(0) + _collider_sort(0), + _owner(nullptr), + _owner_callback(nullptr) { set_cull_callback(); set_renderable(); @@ -60,7 +62,9 @@ CollisionNode(const CollisionNode ©) : PandaNode(copy), _from_collide_mask(copy._from_collide_mask), _collider_sort(copy._collider_sort), - _solids(copy._solids) + _solids(copy._solids), + _owner(nullptr), + _owner_callback(nullptr) { } @@ -69,6 +73,10 @@ CollisionNode(const CollisionNode ©) : */ CollisionNode:: ~CollisionNode() { + if (_owner_callback != nullptr) { + _owner_callback(_owner); + _owner_callback = nullptr; + } } /** @@ -251,6 +259,18 @@ set_from_collide_mask(CollideMask mask) { _from_collide_mask = mask; } +/** + * Removes the owner that was previously set using set_owner(). + */ +void CollisionNode:: +clear_owner() { + if (_owner_callback != nullptr) { + _owner_callback(_owner); + } + _owner = nullptr; + _owner_callback = nullptr; +} + /** * Called when needed to recompute the node's _internal_bound object. Nodes * that contain anything of substance should redefine this to do the right diff --git a/panda/src/collide/collisionNode.h b/panda/src/collide/collisionNode.h index fee71fce78..14941a5336 100644 --- a/panda/src/collide/collisionNode.h +++ b/panda/src/collide/collisionNode.h @@ -77,6 +77,22 @@ PUBLISHED: INLINE static CollideMask get_default_collide_mask(); MAKE_PROPERTY(default_collide_mask, get_default_collide_mask); +public: + typedef void (OwnerCallback)(void *); + + INLINE void *get_owner() const; + +#ifndef CPPPARSER + INLINE void set_owner(void *owner, OwnerCallback *callback = nullptr); + void clear_owner(); +#endif + + EXTENSION(PyObject *get_owner() const); + EXTENSION(void set_owner(PyObject *owner)); + +PUBLISHED: + MAKE_PROPERTY(owner, get_owner, set_owner); + protected: virtual void compute_internal_bounds(CPT(BoundingVolume) &internal_bounds, int &internal_vertices, @@ -94,6 +110,9 @@ private: typedef pvector< COWPT(CollisionSolid) > Solids; Solids _solids; + void *_owner = nullptr; + OwnerCallback *_owner_callback = nullptr; + friend class CollisionTraverser; public: diff --git a/panda/src/collide/collisionNode_ext.cxx b/panda/src/collide/collisionNode_ext.cxx new file mode 100644 index 0000000000..ae42a0d7ef --- /dev/null +++ b/panda/src/collide/collisionNode_ext.cxx @@ -0,0 +1,62 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file collisionNode_ext.cxx + * @author rdb + * @date 2024-12-12 + */ + +#include "collisionNode_ext.h" + +#ifdef HAVE_PYTHON + +#include "collisionNode.h" + +/** + * Returns the object previously set via set_owner(). If the object has been + * destroyed, returns None. + */ +PyObject *Extension:: +get_owner() const { + PyObject *owner = (PyObject *)_this->get_owner(); + +#if PY_VERSION_HEX >= 0x030D0000 // 3.13 + PyObject *strong_ref; + int result = 0; + if (owner != nullptr) { + PyWeakref_GetRef(owner, &strong_ref); + } + if (result > 0) { + return strong_ref; + } + else if (result == 0) { + return Py_NewRef(Py_None); + } + else { + return nullptr; + } +#else + return Py_NewRef(owner != nullptr ? PyWeakref_GetObject(owner) : Py_None); +#endif +} + +/** + * Stores a weak reference to the given object on the CollisionNode, for later + * use in collision events and handlers. + */ +void Extension:: +set_owner(PyObject *owner) { + if (owner != Py_None) { + PyObject *ref = PyWeakref_NewRef(owner, nullptr); + _this->set_owner(ref, [](void *obj) { Py_DECREF((PyObject *)obj); }); + } else { + _this->clear_owner(); + } +} + +#endif diff --git a/panda/src/collide/collisionNode_ext.h b/panda/src/collide/collisionNode_ext.h new file mode 100644 index 0000000000..ea27e1bb7d --- /dev/null +++ b/panda/src/collide/collisionNode_ext.h @@ -0,0 +1,40 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file collisionNode_ext.h + * @author rdb + * @date 2024-12-12 + */ + +#ifndef COLLISIONNODE_EXT_H +#define COLLISIONNODE_EXT_H + +#include "pandabase.h" + +#ifdef HAVE_PYTHON + +#include "extension.h" +#include "collisionNode.h" +#include "py_panda.h" + +/** + * This class defines the extension methods for CollisionNode, which are called + * instead of any C++ methods with the same prototype. + * + * @since 1.11.0 + */ +template<> +class Extension : public ExtensionBase { +public: + PyObject *get_owner() const; + void set_owner(PyObject *owner); +}; + +#endif // HAVE_PYTHON + +#endif diff --git a/panda/src/collide/p3collide_ext_composite.cxx b/panda/src/collide/p3collide_ext_composite.cxx index 76e458c0f3..8e9f584c0c 100644 --- a/panda/src/collide/p3collide_ext_composite.cxx +++ b/panda/src/collide/p3collide_ext_composite.cxx @@ -1,5 +1,6 @@ #include "collisionHandlerEvent_ext.cxx" #include "collisionHandlerPhysical_ext.cxx" #include "collisionHandlerQueue_ext.cxx" +#include "collisionNode_ext.cxx" #include "collisionPolygon_ext.cxx" #include "collisionTraverser_ext.cxx" diff --git a/panda/src/pgraph/nodePath.h b/panda/src/pgraph/nodePath.h index b8f82f779f..21328a5888 100644 --- a/panda/src/pgraph/nodePath.h +++ b/panda/src/pgraph/nodePath.h @@ -884,6 +884,8 @@ PUBLISHED: INLINE void set_collide_mask(CollideMask new_mask, CollideMask bits_to_change = CollideMask::all_on(), TypeHandle node_type = TypeHandle::none()); + EXTENSION(void set_collide_owner(PyObject *owner)); + // Comparison methods INLINE bool operator == (const NodePath &other) const; INLINE bool operator != (const NodePath &other) const; diff --git a/panda/src/pgraph/nodePath_ext.cxx b/panda/src/pgraph/nodePath_ext.cxx index e05877be53..060a96e44d 100644 --- a/panda/src/pgraph/nodePath_ext.cxx +++ b/panda/src/pgraph/nodePath_ext.cxx @@ -15,6 +15,7 @@ #include "typedWritable_ext.h" #include "shaderInput_ext.h" #include "shaderAttrib.h" +#include "collisionNode.h" #ifdef HAVE_PYTHON @@ -327,4 +328,62 @@ get_tight_bounds(const NodePath &other) const { } } +/** + * Recursively assigns a weak reference to the given owner object to all + * collision nodes at this level and below. + * + * You may pass in None to clear all owners below this level. + * + * Note that there is no corresponding get_collide_owner(), since there may be + * multiple nodes below this level with different owners. + */ +void Extension:: +set_collide_owner(PyObject *owner) { + if (owner != Py_None) { + PyObject *ref = PyWeakref_NewRef(owner, nullptr); + if (ref != nullptr) { + r_set_collide_owner(_this->node(), ref); + Py_DECREF(ref); + } + } else { + r_clear_collide_owner(_this->node()); + } +} + +/** + * Recursive implementation of set_collide_owner. weakref must be a weak ref + * object. + */ +void Extension:: +r_set_collide_owner(PandaNode *node, PyObject *weakref) { + if (node->is_collision_node()) { + CollisionNode *cnode = (CollisionNode *)node; + cnode->set_owner(Py_NewRef(weakref), + [](void *obj) { Py_DECREF((PyObject *)obj); }); + } + + PandaNode::Children cr = node->get_children(); + int num_children = cr.get_num_children(); + for (int i = 0; i < num_children; i++) { + r_set_collide_owner(cr.get_child(i), weakref); + } +} + +/** + * Recursive implementation of set_collide_owner(None). + */ +void Extension:: +r_clear_collide_owner(PandaNode *node) { + if (node->is_collision_node()) { + CollisionNode *cnode = (CollisionNode *)node; + cnode->clear_owner(); + } + + PandaNode::Children cr = node->get_children(); + int num_children = cr.get_num_children(); + for (int i = 0; i < num_children; i++) { + r_clear_collide_owner(cr.get_child(i)); + } +} + #endif // HAVE_PYTHON diff --git a/panda/src/pgraph/nodePath_ext.h b/panda/src/pgraph/nodePath_ext.h index b1127f930d..b6f03b792c 100644 --- a/panda/src/pgraph/nodePath_ext.h +++ b/panda/src/pgraph/nodePath_ext.h @@ -54,6 +54,12 @@ public: void set_shader_inputs(PyObject *args, PyObject *kwargs); PyObject *get_tight_bounds(const NodePath &other = NodePath()) const; + + void set_collide_owner(PyObject *owner); + +private: + static void r_set_collide_owner(PandaNode *node, PyObject *weakref); + static void r_clear_collide_owner(PandaNode *node); }; BEGIN_PUBLISH diff --git a/tests/collide/test_collision_node.py b/tests/collide/test_collision_node.py new file mode 100644 index 0000000000..20197e956f --- /dev/null +++ b/tests/collide/test_collision_node.py @@ -0,0 +1,46 @@ +from panda3d.core import * +import sys + + +class CustomObject: + pass + + +def test_collision_node_owner(): + owner = CustomObject() + initial_rc = sys.getrefcount(owner) + + node = CollisionNode("node") + assert node.owner is None + + node.owner = owner + assert sys.getrefcount(owner) == initial_rc + assert node.owner is owner + + node.owner = owner + assert sys.getrefcount(owner) == initial_rc + assert node.owner is owner + + node.owner = None + assert sys.getrefcount(owner) == initial_rc + assert node.owner is None + + del node + assert sys.getrefcount(owner) == initial_rc + + # Assign owner and then delete node + node = CollisionNode("node") + assert sys.getrefcount(owner) == initial_rc + node.owner = owner + assert sys.getrefcount(owner) == initial_rc + del node + assert sys.getrefcount(owner) == initial_rc + + # Delete owner and see what happens to the node + node = CollisionNode("node") + assert sys.getrefcount(owner) == initial_rc + node.owner = owner + assert sys.getrefcount(owner) == initial_rc + del owner + assert node.owner is None + diff --git a/tests/pgraph/test_nodepath.py b/tests/pgraph/test_nodepath.py index ef9d44ab33..f426530429 100644 --- a/tests/pgraph/test_nodepath.py +++ b/tests/pgraph/test_nodepath.py @@ -312,3 +312,47 @@ def test_nodepath_replace_texture_none(): assert path2.get_texture() == tex1 path1.replace_texture(tex1, None) assert path2.get_texture() is None + + +def test_nodepath_set_collide_owner(): + from panda3d.core import NodePath, CollisionNode + + class CustomOwner: + pass + + owner1 = CustomOwner() + owner2 = CustomOwner() + owner3 = CustomOwner() + + root = NodePath("root") + model1 = root.attach_new_node("model1") + collider1 = model1.attach_new_node(CollisionNode("collider1")) + collider2 = model1.attach_new_node(CollisionNode("collider2")) + model2 = root.attach_new_node("model2") + collider3 = model2.attach_new_node(CollisionNode("collider3")) + + root.set_collide_owner(owner1) + assert collider1.node().owner is owner1 + assert collider2.node().owner is owner1 + assert collider3.node().owner is owner1 + + model1.set_collide_owner(None) + assert collider1.node().owner is None + assert collider2.node().owner is None + assert collider3.node().owner is owner1 + + collider2.set_collide_owner(owner2) + assert collider1.node().owner is None + assert collider2.node().owner is owner2 + assert collider3.node().owner is owner1 + + del owner1 + assert collider1.node().owner is None + assert collider2.node().owner is owner2 + assert collider3.node().owner is None + + root.set_collide_owner(owner3) + model2.set_collide_owner(owner2) + assert collider1.node().owner is owner3 + assert collider2.node().owner is owner3 + assert collider3.node().owner is owner2