Bladeren bron

Merge branch 'release/1.10.x'

rdb 3 jaren geleden
bovenliggende
commit
92185bcb28

+ 33 - 27
direct/src/motiontrail/MotionTrail.py

@@ -49,9 +49,9 @@ class MotionTrailFrame:
 class MotionTrail(NodePath, DirectObject):
     """Generates smooth geometry-based motion trails behind a moving object.
 
-    To use this class, first define the shape of the cross-section of the trail
-    by repeatedly calling `add_vertex()` and `set_vertex_color()`.
-    When this is done, `update_vertices()` must be called.
+    To use this class, first define the shape of the cross-section polygon that
+    is to be extruded along the motion trail by calling `add_vertex()` and
+    `set_vertex_color()`.  When this is done, call `update_vertices()`.
 
     To generate the motion trail, either call `register_motion_trail()`
     to have Panda update it automatically, or periodically call the method
@@ -82,6 +82,10 @@ class MotionTrail(NodePath, DirectObject):
 
     @classmethod
     def setGlobalEnable(cls, enable):
+        """Set this to False to have the task stop updating all motion trails.
+        This does not prevent updating them manually using the
+        `update_motion_trail()` method.
+        """
         cls.global_enable = enable
 
     def __init__(self, name, parent_node_path):
@@ -119,14 +123,14 @@ class MotionTrail(NodePath, DirectObject):
         self.continuous_motion_trail = True
         self.color_scale = 1.0
 
-        ## How long the time window is for which the trail is computed.  Can be
-        ## increased to obtain a longer trail, decreased for a shorter trail.
+        #: How long the time window is for which the trail is computed.  Can be
+        #: increased to obtain a longer trail, decreased for a shorter trail.
         self.time_window = 1.0
 
-        ## How often the trail updates, in seconds.  The default is 0.0, which
-        ## has the trail updated every frame for the smoothest result.  Higher
-        ## values will generate a choppier trail.  The `use_nurbs` option can
-        ## compensate partially for this choppiness, however.
+        #: How often the trail updates, in seconds.  The default is 0.0, which
+        #: has the trail updated every frame for the smoothest result.  Higher
+        #: values will generate a choppier trail.  The `use_nurbs` option can
+        #: compensate partially for this choppiness, however.
         self.sampling_time = 0.0
 
         self.square_t = True
@@ -137,9 +141,9 @@ class MotionTrail(NodePath, DirectObject):
         # node path states
         self.reparentTo(parent_node_path)
 
-        ## A `.GeomNode` object containing the generated geometry.  By default
-        ## parented to the MotionTrail itself, but can be reparented elsewhere
-        ## if necessary.
+        #: A `.GeomNode` object containing the generated geometry.  By default
+        #: parented to the MotionTrail itself, but can be reparented elsewhere
+        #: if necessary.
         self.geom_node = GeomNode("motion_trail")
         self.geom_node.setBoundsType(BoundingVolume.BT_box)
         self.geom_node_path = self.attachNewNode(self.geom_node)
@@ -170,9 +174,11 @@ class MotionTrail(NodePath, DirectObject):
 
         self.relative_to_render = False
 
-        ## Set this to True to use a NURBS curve to generate a smooth trail,
-        ## even if the underlying animation or movement is janky.
+        #: Set this to True to use a NURBS curve to generate a smooth trail,
+        #: even if the underlying animation or movement is janky.
         self.use_nurbs = False
+
+        #: This can be changed to fine-tune the resolution of the NURBS curve.
         self.resolution_distance = 0.5
 
         self.cmotion_trail = CMotionTrail()
@@ -249,7 +255,7 @@ class MotionTrail(NodePath, DirectObject):
 
     def add_vertex(self, vertex_id, vertex_function=None, context=None, *,
                    start_color=(1.0, 1.0, 1.0, 1.0), end_color=(0.0, 0.0, 0.0, 1.0)):
-        """This must be called repeatedly to define the polygon that forms the
+        """This must be called initially to define the polygon that forms the
         cross-section of the generated motion trail geometry.  The first
         argument is a user-defined vertex identifier, the second is a function
         that will be called with three parameters that should return the
@@ -390,21 +396,21 @@ class MotionTrail(NodePath, DirectObject):
 
     def add_geometry_quad(self, v0, v1, v2, v3, c0, c1, c2, c3, t0, t1, t2, t3):
 
-        self.vertex_writer.addData3f(v0 [0], v0 [1], v0 [2])
-        self.vertex_writer.addData3f(v1 [0], v1 [1], v1 [2])
-        self.vertex_writer.addData3f(v2 [0], v2 [1], v2 [2])
-        self.vertex_writer.addData3f(v3 [0], v3 [1], v3 [2])
+        self.vertex_writer.addData3(v0[0], v0[1], v0[2])
+        self.vertex_writer.addData3(v1[0], v1[1], v1[2])
+        self.vertex_writer.addData3(v2[0], v2[1], v2[2])
+        self.vertex_writer.addData3(v3[0], v3[1], v3[2])
 
-        self.color_writer.addData4f(c0)
-        self.color_writer.addData4f(c1)
-        self.color_writer.addData4f(c2)
-        self.color_writer.addData4f(c3)
+        self.color_writer.addData4(c0)
+        self.color_writer.addData4(c1)
+        self.color_writer.addData4(c2)
+        self.color_writer.addData4(c3)
 
         if self.texture is not None:
-            self.texture_writer.addData2f(t0)
-            self.texture_writer.addData2f(t1)
-            self.texture_writer.addData2f(t2)
-            self.texture_writer.addData2f(t3)
+            self.texture_writer.addData2(t0)
+            self.texture_writer.addData2(t1)
+            self.texture_writer.addData2(t2)
+            self.texture_writer.addData2(t3)
 
         vertex_index = self.vertex_index
 

+ 33 - 33
direct/src/showbase/ShowBase.py

@@ -1151,7 +1151,7 @@ class ShowBase(DirectObject.DirectObject):
         Creates the render scene graph, the primary scene graph for
         rendering 3-d geometry.
         """
-        ## This is the root of the 3-D scene graph.
+        #: This is the root of the 3-D scene graph.
         self.render = NodePath('render')
         self.render.setAttrib(RescaleNormalAttrib.makeDefault())
 
@@ -1170,7 +1170,7 @@ class ShowBase(DirectObject.DirectObject):
         # for the benefit of creating DirectGui elements before ShowBase.
         from . import ShowBaseGlobal
 
-        ## This is the root of the 2-D scene graph.
+        #: This is the root of the 2-D scene graph.
         self.render2d = ShowBaseGlobal.render2d
 
         # Set up some overrides to turn off certain properties which
@@ -1191,12 +1191,12 @@ class ShowBase(DirectObject.DirectObject):
         self.render2d.setMaterialOff(1)
         self.render2d.setTwoSided(1)
 
-        ## The normal 2-d DisplayRegion has an aspect ratio that
-        ## matches the window, but its coordinate system is square.
-        ## This means anything we parent to render2d gets stretched.
-        ## For things where that makes a difference, we set up
-        ## aspect2d, which scales things back to the right aspect
-        ## ratio along the X axis (Z is still from -1 to 1)
+        #: The normal 2-d DisplayRegion has an aspect ratio that
+        #: matches the window, but its coordinate system is square.
+        #: This means anything we parent to render2d gets stretched.
+        #: For things where that makes a difference, we set up
+        #: aspect2d, which scales things back to the right aspect
+        #: ratio along the X axis (Z is still from -1 to 1)
         self.aspect2d = ShowBaseGlobal.aspect2d
 
         aspectRatio = self.getAspectRatio()
@@ -1204,13 +1204,13 @@ class ShowBase(DirectObject.DirectObject):
 
         self.a2dBackground = self.aspect2d.attachNewNode("a2dBackground")
 
-        ## The Z position of the top border of the aspect2d screen.
+        #: The Z position of the top border of the aspect2d screen.
         self.a2dTop = 1.0
-        ## The Z position of the bottom border of the aspect2d screen.
+        #: The Z position of the bottom border of the aspect2d screen.
         self.a2dBottom = -1.0
-        ## The X position of the left border of the aspect2d screen.
+        #: The X position of the left border of the aspect2d screen.
         self.a2dLeft = -aspectRatio
-        ## The X position of the right border of the aspect2d screen.
+        #: The X position of the right border of the aspect2d screen.
         self.a2dRight = aspectRatio
 
         self.a2dTopCenter = self.aspect2d.attachNewNode("a2dTopCenter")
@@ -1250,9 +1250,9 @@ class ShowBase(DirectObject.DirectObject):
         self.a2dBottomRight.setPos(self.a2dRight, 0, self.a2dBottom)
         self.a2dBottomRightNs.setPos(self.a2dRight, 0, self.a2dBottom)
 
-        ## This special root, pixel2d, uses units in pixels that are relative
-        ## to the window. The upperleft corner of the window is (0, 0),
-        ## the lowerleft corner is (xsize, -ysize), in this coordinate system.
+        #: This special root, pixel2d, uses units in pixels that are relative
+        #: to the window. The upperleft corner of the window is (0, 0),
+        #: the lowerleft corner is (xsize, -ysize), in this coordinate system.
         self.pixel2d = self.render2d.attachNewNode(PGTop("pixel2d"))
         self.pixel2d.setPos(-1, 0, 1)
         xsize, ysize = self.getSize()
@@ -1282,25 +1282,25 @@ class ShowBase(DirectObject.DirectObject):
         self.render2dp.setMaterialOff(1)
         self.render2dp.setTwoSided(1)
 
-        ## The normal 2-d DisplayRegion has an aspect ratio that
-        ## matches the window, but its coordinate system is square.
-        ## This means anything we parent to render2dp gets stretched.
-        ## For things where that makes a difference, we set up
-        ## aspect2dp, which scales things back to the right aspect
-        ## ratio along the X axis (Z is still from -1 to 1)
+        #: The normal 2-d DisplayRegion has an aspect ratio that
+        #: matches the window, but its coordinate system is square.
+        #: This means anything we parent to render2dp gets stretched.
+        #: For things where that makes a difference, we set up
+        #: aspect2dp, which scales things back to the right aspect
+        #: ratio along the X axis (Z is still from -1 to 1)
         self.aspect2dp = self.render2dp.attachNewNode(PGTop("aspect2dp"))
         self.aspect2dp.node().setStartSort(16384)
 
         aspectRatio = self.getAspectRatio()
         self.aspect2dp.setScale(1.0 / aspectRatio, 1.0, 1.0)
 
-        ## The Z position of the top border of the aspect2dp screen.
+        #: The Z position of the top border of the aspect2dp screen.
         self.a2dpTop = 1.0
-        ## The Z position of the bottom border of the aspect2dp screen.
+        #: The Z position of the bottom border of the aspect2dp screen.
         self.a2dpBottom = -1.0
-        ## The X position of the left border of the aspect2dp screen.
+        #: The X position of the left border of the aspect2dp screen.
         self.a2dpLeft = -aspectRatio
-        ## The X position of the right border of the aspect2dp screen.
+        #: The X position of the right border of the aspect2dp screen.
         self.a2dpRight = aspectRatio
 
         self.a2dpTopCenter = self.aspect2dp.attachNewNode("a2dpTopCenter")
@@ -1324,9 +1324,9 @@ class ShowBase(DirectObject.DirectObject):
         self.a2dpBottomLeft.setPos(self.a2dpLeft, 0, self.a2dpBottom)
         self.a2dpBottomRight.setPos(self.a2dpRight, 0, self.a2dpBottom)
 
-        ## This special root, pixel2d, uses units in pixels that are relative
-        ## to the window. The upperleft corner of the window is (0, 0),
-        ## the lowerleft corner is (xsize, -ysize), in this coordinate system.
+        #: This special root, pixel2dp, uses units in pixels that are relative
+        #: to the window. The upperleft corner of the window is (0, 0),
+        #: the lowerleft corner is (xsize, -ysize), in this coordinate system.
         self.pixel2dp = self.render2dp.attachNewNode(PGTop("pixel2dp"))
         self.pixel2dp.node().setStartSort(16384)
         self.pixel2dp.setPos(-1, 0, 1)
@@ -1647,11 +1647,11 @@ class ShowBase(DirectObject.DirectObject):
 
         mw = self.buttonThrowers[0].getParent()
 
-        ## A special ButtonThrower to generate keyboard events and
-        ## include the time from the OS.  This is separate only to
-        ## support legacy code that did not expect a time parameter; it
-        ## will eventually be folded into the normal ButtonThrower,
-        ## above.
+        #: A special ButtonThrower to generate keyboard events and
+        #: include the time from the OS.  This is separate only to
+        #: support legacy code that did not expect a time parameter; it
+        #: will eventually be folded into the normal ButtonThrower,
+        #: above.
         self.timeButtonThrower = mw.attachNewNode(ButtonThrower('timeButtons'))
         self.timeButtonThrower.node().setPrefix('time-')
         self.timeButtonThrower.node().setTimeFlag(1)

+ 4 - 0
doc/ReleaseNotes

@@ -71,6 +71,8 @@ Miscellaneous
 * Fix texture transforms sometimes not being flattened (#1392)
 * Fix support for `#pragma include <file.glsl>` in GLSL shaders
 * Fix `ShaderBuffer.prepare()` not doing anything
+* Implement deepcopy for PointerToArray
+* Fix Texture deepcopy keeping a reference to the original RAM image
 * Fix bf-cbc encryption no longer working when building with OpenSSL 3.0
 * PandaNode bounds_type property was erroneously marked read-only
 * Fix warnings when copying OdeTriMeshGeom objects
@@ -81,8 +83,10 @@ Miscellaneous
 * Add various useful functions to interrogatedb module
 * Fix Python 3 issues unpacking uint types in Python 3 (#1380)
 * Fix interrogate syntax error with C++11-style attributes in declarators
+* Fix double-precision color values not being clamped by GeomVertexWriter
 * Fix regression with BufferViewer in double-precision build (#1365)
 * Fix `PandaNode.nested_vertices` not updating properly
+* Prevent Panda calculating bounding volume of Geom with custom bounding volume
 * Add `do_events()` and `process_event()` snake_case aliases in eventMgr
 * Support second arg of None in `replace_texture()` / `replace_material()`
 * Support `os.fspath()` for ConfigVariableFilename objects (#1406)

+ 2 - 1
panda/src/cocoadisplay/cocoaPandaAppDelegate.mm

@@ -39,7 +39,8 @@
   // Ask all the windows whether they are OK to be closed.
   bool should_close = true;
   for (NSWindow *window in [app windows]) {
-    if (![[window delegate] windowShouldClose:window]) {
+    id<NSWindowDelegate> delegate = [window delegate];
+    if (delegate != nil && ![delegate windowShouldClose:window]) {
       should_close = false;
     }
   }

+ 4 - 0
panda/src/express/pointerToArray.h

@@ -121,6 +121,8 @@ PUBLISHED:
 
   EXTENSION(int __getbuffer__(PyObject *self, Py_buffer *view, int flags));
   EXTENSION(void __releasebuffer__(PyObject *self, Py_buffer *view) const);
+
+  EXTENSION(PointerToArray<Element> __deepcopy__(PyObject *memo) const);
 #endif
 
 #else  // CPPPARSER
@@ -279,6 +281,8 @@ PUBLISHED:
 
   EXTENSION(int __getbuffer__(PyObject *self, Py_buffer *view, int flags) const);
   EXTENSION(void __releasebuffer__(PyObject *self, Py_buffer *view) const);
+
+  EXTENSION(ConstPointerToArray<Element> __deepcopy__(PyObject *memo) const);
 #endif
 
 #else  // CPPPARSER

+ 28 - 0
panda/src/express/pointerToArray_ext.I

@@ -440,6 +440,20 @@ __releasebuffer__(PyObject *self, Py_buffer *view) const {
   }
 }
 
+/**
+ * A special Python method that is invoked by copy.deepcopy(pta).  This makes
+ * sure that there is truly a unique copy of the array.
+ */
+template<class Element>
+INLINE PointerToArray<Element> Extension<PointerToArray<Element> >::
+__deepcopy__(PyObject *memo) const {
+  PointerToArray<Element> copy;
+  if (!this->_this->is_null()) {
+    copy.v() = this->_this->v();
+  }
+  return copy;
+}
+
 /**
  * This is used to implement the buffer protocol, in order to allow efficient
  * access to the array data through a Python multiview object.
@@ -610,3 +624,17 @@ __releasebuffer__(PyObject *self, Py_buffer *view) const {
     view->internal = nullptr;
   }
 }
+
+/**
+ * A special Python method that is invoked by copy.deepcopy(pta).  This makes
+ * sure that there is truly a unique copy of the array.
+ */
+template<class Element>
+INLINE ConstPointerToArray<Element> Extension<ConstPointerToArray<Element> >::
+__deepcopy__(PyObject *memo) const {
+  PointerToArray<Element> copy;
+  if (!this->_this->is_null()) {
+    copy.v() = this->_this->v();
+  }
+  return copy;
+}

+ 4 - 0
panda/src/express/pointerToArray_ext.h

@@ -44,6 +44,8 @@ public:
 
   INLINE int __getbuffer__(PyObject *self, Py_buffer *view, int flags);
   INLINE void __releasebuffer__(PyObject *self, Py_buffer *view) const;
+
+  INLINE PointerToArray<Element> __deepcopy__(PyObject *memo) const;
 };
 
 template<>
@@ -81,6 +83,8 @@ public:
 
   INLINE int __getbuffer__(PyObject *self, Py_buffer *view, int flags) const;
   INLINE void __releasebuffer__(PyObject *self, Py_buffer *view) const;
+
+  INLINE ConstPointerToArray<Element> __deepcopy__(PyObject *memo) const;
 };
 
 template<>

+ 50 - 0
panda/src/gobj/geomVertexColumn.cxx

@@ -4472,6 +4472,20 @@ get_data4f(const unsigned char *pointer) {
   return _v4;
 }
 
+/**
+ *
+ */
+const LVecBase4d &GeomVertexColumn::Packer_argb_packed::
+get_data4d(const unsigned char *pointer) {
+  uint32_t dword = *(const uint32_t *)pointer;
+  _v4d.set(GeomVertexData::unpack_abcd_b(dword),
+           GeomVertexData::unpack_abcd_c(dword),
+           GeomVertexData::unpack_abcd_d(dword),
+           GeomVertexData::unpack_abcd_a(dword));
+  _v4d /= 255.0;
+  return _v4d;
+}
+
 /**
  *
  */
@@ -4486,6 +4500,20 @@ set_data4f(unsigned char *pointer, const LVecBase4f &data) {
      (unsigned int)(min(max(data[2], 0.0f), 1.0f) * 255.0f));
 }
 
+/**
+ *
+ */
+void GeomVertexColumn::Packer_argb_packed::
+set_data4d(unsigned char *pointer, const LVecBase4d &data) {
+  // when packing an argb, we want to make sure we cap the input values at 1
+  // since going above one will cause the value to be truncated.
+  *(uint32_t *)pointer = GeomVertexData::pack_abcd
+    ((unsigned int)(min(max(data[3], 0.0), 1.0) * 255.0),
+     (unsigned int)(min(max(data[0], 0.0), 1.0) * 255.0),
+     (unsigned int)(min(max(data[1], 0.0), 1.0) * 255.0),
+     (unsigned int)(min(max(data[2], 0.0), 1.0) * 255.0));
+}
+
 /**
  *
  */
@@ -4497,6 +4525,17 @@ get_data4f(const unsigned char *pointer) {
   return _v4;
 }
 
+/**
+ *
+ */
+const LVecBase4d &GeomVertexColumn::Packer_rgba_uint8_4::
+get_data4d(const unsigned char *pointer) {
+  _v4d.set((double)pointer[0], (double)pointer[1],
+           (double)pointer[2], (double)pointer[3]);
+  _v4d /= 255.0;
+  return _v4d;
+}
+
 /**
  *
  */
@@ -4508,6 +4547,17 @@ set_data4f(unsigned char *pointer, const LVecBase4f &data) {
   pointer[3] = (unsigned int)(min(max(data[3], 0.0f), 1.0f) * 255.0f);
 }
 
+/**
+ *
+ */
+void GeomVertexColumn::Packer_rgba_uint8_4::
+set_data4d(unsigned char *pointer, const LVecBase4d &data) {
+  pointer[0] = (unsigned int)(min(max(data[0], 0.0), 1.0) * 255.0);
+  pointer[1] = (unsigned int)(min(max(data[1], 0.0), 1.0) * 255.0);
+  pointer[2] = (unsigned int)(min(max(data[2], 0.0), 1.0) * 255.0);
+  pointer[3] = (unsigned int)(min(max(data[3], 0.0), 1.0) * 255.0);
+}
+
 /**
  *
  */

+ 4 - 0
panda/src/gobj/geomVertexColumn.h

@@ -381,7 +381,9 @@ private:
   class Packer_argb_packed final : public Packer_color {
   public:
     virtual const LVecBase4f &get_data4f(const unsigned char *pointer);
+    virtual const LVecBase4d &get_data4d(const unsigned char *pointer);
     virtual void set_data4f(unsigned char *pointer, const LVecBase4f &value);
+    virtual void set_data4d(unsigned char *pointer, const LVecBase4d &value);
 
     virtual const char *get_name() const {
       return "Packer_argb_packed";
@@ -391,7 +393,9 @@ private:
   class Packer_rgba_uint8_4 final : public Packer_color {
   public:
     virtual const LVecBase4f &get_data4f(const unsigned char *pointer);
+    virtual const LVecBase4d &get_data4d(const unsigned char *pointer);
     virtual void set_data4f(unsigned char *pointer, const LVecBase4f &value);
+    virtual void set_data4d(unsigned char *pointer, const LVecBase4d &value);
 
     virtual const char *get_name() const {
       return "Packer_rgba_uint8_4";

+ 4 - 0
panda/src/gobj/texture.h

@@ -46,6 +46,7 @@
 #include "pnmImage.h"
 #include "pfmFile.h"
 #include "asyncTask.h"
+#include "extension.h"
 
 class TextureContext;
 class FactoryParams;
@@ -473,6 +474,8 @@ PUBLISHED:
   MAKE_PROPERTY(keep_ram_image, get_keep_ram_image, set_keep_ram_image);
   MAKE_PROPERTY(cacheable, is_cacheable);
 
+  EXTENSION(PT(Texture) __deepcopy__(PyObject *memo) const);
+
   BLOCKING INLINE bool compress_ram_image(CompressionMode compression = CM_on,
                                           QualityLevel quality_level = QL_default,
                                           GraphicsStateGuardianBase *gsg = nullptr);
@@ -1122,6 +1125,7 @@ private:
 
   static TypeHandle _type_handle;
 
+  friend class Extension<Texture>;
   friend class TextureContext;
   friend class PreparedGraphicsObjects;
   friend class TexturePool;

+ 28 - 0
panda/src/gobj/texture_ext.cxx

@@ -138,4 +138,32 @@ set_ram_image_as(PyObject *image, const std::string &provided_format) {
   Dtool_Raise_ArgTypeError(image, 0, "Texture.set_ram_image_as", "CPTA_uchar or buffer");
 }
 
+/**
+ * A special Python method that is invoked by copy.deepcopy(tex).  This makes
+ * sure that the copy has a unique copy of the RAM image.
+ */
+PT(Texture) Extension<Texture>::
+__deepcopy__(PyObject *memo) const {
+  PT(Texture) copy = _this->make_copy();
+  {
+    Texture::CDWriter cdata(copy->_cycler, true);
+    for (Texture::RamImage &image : cdata->_ram_images) {
+      if (image._image.get_ref_count() > 1) {
+        PTA_uchar new_image;
+        new_image.v() = image._image.v();
+        image._image = std::move(new_image);
+      }
+    }
+    {
+      Texture::RamImage &image = cdata->_simple_ram_image;
+      if (image._image.get_ref_count() > 1) {
+        PTA_uchar new_image;
+        new_image.v() = image._image.v();
+        image._image = std::move(new_image);
+      }
+    }
+  }
+  return copy;
+}
+
 #endif  // HAVE_PYTHON

+ 2 - 0
panda/src/gobj/texture_ext.h

@@ -32,6 +32,8 @@ public:
   void set_ram_image(PyObject *image, Texture::CompressionMode compression = Texture::CM_off,
                      size_t page_size = 0);
   void set_ram_image_as(PyObject *image, const std::string &provided_format);
+
+  PT(Texture) __deepcopy__(PyObject *memo) const;
 };
 
 #endif  // HAVE_PYTHON

+ 60 - 0
tests/express/test_pointertoarray.py

@@ -69,3 +69,63 @@ def test_cpta_float_pickle():
         data_pta2 = loads(dumps(data_pta, proto))
         assert tuple(data_pta2) == (1.0, 2.0, 3.0)
         assert data_pta2.get_data() == data_pta.get_data()
+
+
+def test_pta_float_copy():
+    from panda3d.core import PTA_float
+    from copy import copy
+
+    null_pta = PTA_float()
+    assert copy(null_pta).is_null()
+
+    empty_pta = PTA_float([])
+    empty_pta_copy = copy(empty_pta)
+    assert not empty_pta_copy.is_null()
+    assert len(empty_pta_copy) == 0
+    assert empty_pta_copy.get_ref_count() == 2
+
+    data_pta = PTA_float([1.0, 2.0, 3.0])
+    data_pta_copy = copy(data_pta)
+    assert not data_pta_copy.is_null()
+    assert data_pta_copy.get_ref_count() == 2
+    assert tuple(data_pta_copy) == (1.0, 2.0, 3.0)
+
+
+def test_pta_float_deepcopy():
+    from panda3d.core import PTA_float
+    from copy import deepcopy
+
+    null_pta = PTA_float()
+    assert deepcopy(null_pta).is_null()
+
+    empty_pta = PTA_float([])
+    empty_pta_copy = deepcopy(empty_pta)
+    assert not empty_pta_copy.is_null()
+    assert len(empty_pta_copy) == 0
+    assert empty_pta_copy.get_ref_count() == 1
+
+    data_pta = PTA_float([1.0, 2.0, 3.0])
+    data_pta_copy = deepcopy(data_pta)
+    assert not data_pta_copy.is_null()
+    assert data_pta_copy.get_ref_count() == 1
+    assert tuple(data_pta_copy) == (1.0, 2.0, 3.0)
+
+
+def test_cpta_float_deepcopy():
+    from panda3d.core import PTA_float, CPTA_float
+    from copy import deepcopy
+
+    null_pta = CPTA_float(PTA_float())
+    assert deepcopy(null_pta).is_null()
+
+    empty_pta = CPTA_float([])
+    empty_pta_copy = deepcopy(empty_pta)
+    assert not empty_pta_copy.is_null()
+    assert len(empty_pta_copy) == 0
+    assert empty_pta_copy.get_ref_count() == 1
+
+    data_pta = CPTA_float([1.0, 2.0, 3.0])
+    data_pta_copy = deepcopy(data_pta)
+    assert not data_pta_copy.is_null()
+    assert data_pta_copy.get_ref_count() == 1
+    assert tuple(data_pta_copy) == (1.0, 2.0, 3.0)

+ 23 - 0
tests/gobj/test_texture.py

@@ -134,3 +134,26 @@ def test_texture_clear_half():
     assert col.y == -inf
     assert col.z == -inf
     assert math.isnan(col.w)
+
+
+def test_texture_deepcopy():
+    from copy import deepcopy
+
+    empty_tex = Texture("empty-texture")
+    empty_tex.setup_2d_texture(16, 16, Texture.T_unsigned_byte, Texture.F_rgba)
+    assert not empty_tex.has_ram_image()
+    empty_tex2 = deepcopy(empty_tex)
+    assert empty_tex2.name == empty_tex.name
+    assert not empty_tex2.has_ram_image()
+
+    tex = Texture("texture")
+    tex.setup_2d_texture(16, 16, Texture.T_unsigned_byte, Texture.F_rgba)
+    img = tex.make_ram_image()
+    assert tex.has_ram_image()
+    assert img.get_ref_count() == 2
+
+    tex2 = deepcopy(tex)
+    assert tex2.name == tex.name
+    assert tex2.has_ram_image()
+    img2 = tex2.get_ram_image()
+    assert img2.get_ref_count() == 2