Browse Source

Merge branch 'release/1.10.x'

rdb 6 years ago
parent
commit
a40228202b
43 changed files with 672 additions and 188 deletions
  1. 4 4
      README.md
  2. 14 7
      direct/src/dist/FreezeTool.py
  3. 56 0
      direct/src/dist/commands.py
  4. 2 1
      direct/src/gui/DirectDialog.py
  5. 9 7
      direct/src/gui/DirectEntry.py
  6. 25 3
      direct/src/gui/DirectEntryScroll.py
  7. 3 4
      direct/src/gui/DirectGuiBase.py
  8. 5 4
      direct/src/gui/DirectScrolledList.py
  9. 199 62
      direct/src/p3d/DeploymentTools.py
  10. 4 1
      direct/src/showbase/ShowBase.py
  11. 1 0
      direct/src/showbase/ShowBaseGlobal.py
  12. 2 1
      direct/src/showbase/Transitions.py
  13. 2 1
      direct/src/tkpanels/MopathRecorder.py
  14. 38 0
      doc/ReleaseNotes
  15. 10 1
      dtool/src/interrogate/interfaceMakerPythonNative.cxx
  16. 8 1
      dtool/src/interrogatedb/dtool_super_base.cxx
  17. 1 1
      dtool/src/interrogatedb/py_compat.h
  18. 73 10
      dtool/src/interrogatedb/py_wrappers.cxx
  19. 4 1
      dtool/src/prc/notify.cxx
  20. 27 0
      makepanda/installer.nsi
  21. 3 3
      makepanda/makepackage.py
  22. 4 3
      makepanda/makepanda.py
  23. 5 1
      makepanda/makepandacore.py
  24. 8 2
      makepanda/makewheel.py
  25. 8 3
      makepanda/test_wheel.py
  26. 18 4
      panda/src/bullet/bulletBodyNode.cxx
  27. 6 3
      panda/src/bullet/bulletContactCallbacks.h
  28. 6 4
      panda/src/bullet/bulletDebugNode.cxx
  29. 3 1
      panda/src/bullet/config_bullet.cxx
  30. 0 6
      panda/src/linmath/lmatrix3_src.I
  31. 6 2
      panda/src/ode/odeGeom.cxx
  32. 8 4
      panda/src/ode/odeJoint.cxx
  33. 5 3
      panda/src/ode/odeSpace.cxx
  34. 3 1
      panda/src/ode/odeTriMeshData.I
  35. 44 20
      panda/src/ode/odeTriMeshData.cxx
  36. 9 3
      panda/src/ode/odeWorld.cxx
  37. 2 0
      panda/src/pgraph/nodePath.cxx
  38. 2 0
      panda/src/pgraph/pythonLoaderFileType.h
  39. 3 3
      panda/src/pgraph/textureAttrib.I
  40. 2 13
      panda/src/pgraph/textureAttrib.cxx
  41. 1 0
      pandatool/src/mayaprogs/mayapath.cxx
  42. 1 0
      setup.cfg
  43. 38 0
      tests/gui/test_DirectEntry.py

+ 4 - 4
README.md

@@ -24,7 +24,7 @@ Installing Panda3D
 ==================
 
 The latest Panda3D SDK can be downloaded from
-[this page](https://www.panda3d.org/download/sdk-1-10-3/).
+[this page](https://www.panda3d.org/download/sdk-1-10-4/).
 If you are familiar with installing Python packages, you can use
 the following comand:
 
@@ -64,8 +64,8 @@ depending on whether you are on a 32-bit or 64-bit system, or you can
 [click here](https://github.com/rdb/panda3d-thirdparty) for instructions on
 building them from source.
 
-https://www.panda3d.org/download/panda3d-1.10.3/panda3d-1.10.3-tools-win64.zip
-https://www.panda3d.org/download/panda3d-1.10.3/panda3d-1.10.3-tools-win32.zip
+https://www.panda3d.org/download/panda3d-1.10.4/panda3d-1.10.4-tools-win64.zip
+https://www.panda3d.org/download/panda3d-1.10.4/panda3d-1.10.4-tools-win32.zip
 
 After acquiring these dependencies, you may simply build Panda3D from the
 command prompt using the following command.  (Change `14.1` to `14` if you are
@@ -135,7 +135,7 @@ macOS
 -----
 
 On macOS, you will need to download a set of precompiled thirdparty packages in order to
-compile Panda3D, which can be acquired from [here](https://www.panda3d.org/download/panda3d-1.10.3/panda3d-1.10.3-tools-mac.tar.gz).
+compile Panda3D, which can be acquired from [here](https://www.panda3d.org/download/panda3d-1.10.4/panda3d-1.10.4-tools-mac.tar.gz).
 
 After placing the thirdparty directory inside the panda3d source directory,
 you may build Panda3D using a command like the following:

+ 14 - 7
direct/src/dist/FreezeTool.py

@@ -800,29 +800,36 @@ class Freezer:
                     self.moduleSuffixes[i] = (suffix[0], 'rb', imp.PY_SOURCE)
         else:
             self.moduleSuffixes = [('.py', 'rb', 1), ('.pyc', 'rb', 2)]
+
+            abi_version = '{0}{1}'.format(*sys.version_info)
+            abi_flags = ''
+            if sys.version_info < (3, 8):
+                abi_flags += 'm'
+
             if 'linux' in self.platform:
                 self.moduleSuffixes += [
-                    ('.cpython-{0}{1}m-x86_64-linux-gnu.so'.format(*sys.version_info), 'rb', 3),
-                    ('.cpython-{0}{1}m-i686-linux-gnu.so'.format(*sys.version_info), 'rb', 3),
+                    ('.cpython-{0}{1}-x86_64-linux-gnu.so'.format(abi_version, abi_flags), 'rb', 3),
+                    ('.cpython-{0}{1}-i686-linux-gnu.so'.format(abi_version, abi_flags), 'rb', 3),
                     ('.abi{0}.so'.format(sys.version_info[0]), 'rb', 3),
                     ('.so', 'rb', 3),
                 ]
             elif 'win' in self.platform:
+                # ABI flags are not appended on Windows.
                 self.moduleSuffixes += [
-                    ('.cp{0}{1}-win_amd64.pyd'.format(*sys.version_info), 'rb', 3),
-                    ('.cp{0}{1}-win32.pyd'.format(*sys.version_info), 'rb', 3),
+                    ('.cp{0}-win_amd64.pyd'.format(abi_version), 'rb', 3),
+                    ('.cp{0}-win32.pyd'.format(abi_version), 'rb', 3),
                     ('.pyd', 'rb', 3),
                 ]
             elif 'mac' in self.platform:
                 self.moduleSuffixes += [
-                    ('.cpython-{0}{1}m-darwin.so'.format(*sys.version_info), 'rb', 3),
+                    ('.cpython-{0}{1}-darwin.so'.format(abi_version, abi_flags), 'rb', 3),
                     ('.abi{0}.so'.format(sys.version_info[0]), 'rb', 3),
                     ('.so', 'rb', 3),
                 ]
             else: # FreeBSD et al.
                 self.moduleSuffixes += [
-                    ('.cpython-{0}{1}m.so'.format(*sys.version_info), 'rb', 3),
-                    ('.abi{0}.so'.format(*sys.version_info), 'rb', 3),
+                    ('.cpython-{0}{1}.so'.format(abi_version, abi_flags), 'rb', 3),
+                    ('.abi{0}.so'.format(sys.version_info[0]), 'rb', 3),
                     ('.so', 'rb', 3),
                 ]
 

+ 56 - 0
direct/src/dist/commands.py

@@ -14,12 +14,14 @@ import struct
 import imp
 import string
 import time
+import tempfile
 
 import setuptools
 import distutils.log
 
 from . import FreezeTool
 from . import pefile
+from direct.p3d.DeploymentTools import Icon
 import panda3d.core as p3d
 
 
@@ -224,6 +226,7 @@ class build_apps(setuptools.Command):
         self.exclude_patterns = []
         self.include_modules = {}
         self.exclude_modules = {}
+        self.icons = {}
         self.platforms = [
             'manylinux1_x86_64',
             'macosx_10_6_x86_64',
@@ -298,6 +301,7 @@ class build_apps(setuptools.Command):
             key: _parse_list(value)
             for key, value in _parse_dict(self.exclude_modules).items()
         }
+        self.icons = _parse_dict(self.icons)
         self.platforms = _parse_list(self.platforms)
         self.plugins = _parse_list(self.plugins)
         self.extra_prc_files = _parse_list(self.extra_prc_files)
@@ -357,6 +361,18 @@ class build_apps(setuptools.Command):
         tmp.update(self.package_data_dirs)
         self.package_data_dirs = tmp
 
+        self.icon_objects = {}
+        for app, iconpaths in self.icons.items():
+            if not isinstance(iconpaths, list) and not isinstance(iconpaths, tuple):
+                iconpaths = (iconpaths,)
+
+            iconobj = Icon()
+            for iconpath in iconpaths:
+                iconobj.addImage(iconpath)
+
+            iconobj.generateMissingImages()
+            self.icon_objects[app] = iconobj
+
     def run(self):
         self.announce('Building platforms: {0}'.format(','.join(self.platforms)), distutils.log.INFO)
 
@@ -433,6 +449,22 @@ class build_apps(setuptools.Command):
 
         return wheelpaths
 
+    def update_pe_resources(self, appname, runtime):
+        """Update resources (e.g., icons) in windows PE file"""
+
+        icon = self.icon_objects.get(
+            appname,
+            self.icon_objects.get('*', None),
+        )
+
+        if icon is not None:
+            pef = pefile.PEFile()
+            pef.open(runtime, 'r+')
+            pef.add_icon(icon)
+            pef.add_resource_section()
+            pef.write_changes()
+            pef.close()
+
     def bundle_macos_app(self, builddir):
         """Bundle built runtime into a .app for macOS"""
 
@@ -474,6 +506,15 @@ class build_apps(setuptools.Command):
             'CFBundleSignature': '', #TODO
             'CFBundleExecutable': self.macos_main_app,
         }
+
+        icon = self.icon_objects.get(
+            self.macos_main_app,
+            self.icon_objects.get('*', None)
+        )
+        if icon is not None:
+            plist['CFBundleIconFile'] = 'iconfile'
+            icon.makeICNS(os.path.join(resdir, 'iconfile.icns'))
+
         with open(os.path.join(contentsdir, 'Info.plist'), 'wb') as f:
             if hasattr(plistlib, 'dump'):
                 plistlib.dump(plist, f)
@@ -618,6 +659,18 @@ class build_apps(setuptools.Command):
                 stub_path = os.path.join(os.path.dirname(dtool_path), '..', 'bin', stub_name)
                 stub_file = open(stub_path, 'rb')
 
+            # Do we need an icon?  On Windows, we need to add this to the stub
+            # before we add the blob.
+            if 'win' in platform:
+                temp_file = tempfile.NamedTemporaryFile(suffix='-icon.exe', delete=False)
+                temp_file.write(stub_file.read())
+                stub_file.close()
+                temp_file.close()
+                self.update_pe_resources(appname, temp_file.name)
+                stub_file = open(temp_file.name, 'rb')
+            else:
+                temp_file = None
+
             freezer.generateRuntimeFromStub(target_path, stub_file, use_console, {
                 'prc_data': prcexport if self.embed_prc_data else None,
                 'default_prc_dir': self.default_prc_dir,
@@ -633,6 +686,9 @@ class build_apps(setuptools.Command):
             }, self.log_append)
             stub_file.close()
 
+            if temp_file:
+                os.unlink(temp_file.name)
+
             # Copy the dependencies.
             search_path = [builddir]
             if use_wheels:

+ 2 - 1
direct/src/gui/DirectDialog.py

@@ -3,6 +3,7 @@
 __all__ = ['findDialog', 'cleanupDialog', 'DirectDialog', 'OkDialog', 'OkCancelDialog', 'YesNoDialog', 'YesNoCancelDialog', 'RetryCancelDialog']
 
 from panda3d.core import *
+from direct.showbase import ShowBaseGlobal
 from . import DirectGuiGlobals as DGG
 from .DirectFrame import *
 from .DirectButton import *
@@ -207,7 +208,7 @@ class DirectDialog(DirectFrame):
             image = None
         # Get size of text/geom without image (for state 0)
         if image:
-            image.reparentTo(hidden)
+            image.reparentTo(ShowBaseGlobal.hidden)
         bounds = self.stateNodePath[0].getTightBounds()
         if image:
             image.reparentTo(self.stateNodePath[0])

+ 9 - 7
direct/src/gui/DirectEntry.py

@@ -4,6 +4,7 @@ text entered using the keyboard."""
 __all__ = ['DirectEntry']
 
 from panda3d.core import *
+from direct.showbase import ShowBaseGlobal
 from . import DirectGuiGlobals as DGG
 from .DirectFrame import *
 from .OnscreenText import OnscreenText
@@ -94,7 +95,7 @@ class DirectEntry(DirectFrame):
         self.onscreenText = self.createcomponent(
             'text', (), None,
             OnscreenText,
-            (), parent = hidden,
+            (), parent = ShowBaseGlobal.hidden,
             # Pass in empty text to avoid extra work, since its really
             # The PGEntry which will use the TextNode to generate geometry
             text = '',
@@ -215,11 +216,11 @@ class DirectEntry(DirectFrame):
         self._autoCapitalize()
 
     def _autoCapitalize(self):
-        name = self.get().decode('utf-8')
+        name = self.guiItem.getWtext()
         # capitalize each word, allowing for things like McMutton
-        capName = ''
+        capName = u''
         # track each individual word to detect prefixes like Mc
-        wordSoFar = ''
+        wordSoFar = u''
         # track whether the previous character was part of a word or not
         wasNonWordChar = True
         for i, character in enumerate(name):
@@ -228,9 +229,9 @@ class DirectEntry(DirectFrame):
             #   This assumes that string.lower and string.upper will return different
             #   values for all unicode letters.
             # - Don't count apostrophes as a break between words
-            if character.lower() == character.upper() and character != "'":
+            if character.lower() == character.upper() and character != u"'":
                 # we are between words
-                wordSoFar = ''
+                wordSoFar = u''
                 wasNonWordChar = True
             else:
                 capitalize = False
@@ -254,7 +255,8 @@ class DirectEntry(DirectFrame):
                 wordSoFar += character
                 wasNonWordChar = False
             capName += character
-        self.enterText(capName.encode('utf-8'))
+        self.guiItem.setWtext(capName)
+        self.guiItem.setCursorPosition(self.guiItem.getNumCharacters())
 
     def focusOutCommandFunc(self):
         if self['focusOutCommand']:

+ 25 - 3
direct/src/gui/DirectEntryScroll.py

@@ -29,19 +29,41 @@ class DirectEntryScroll(DirectFrame):
            # frameSize = (-0.006, 3.2, -0.015, 0.036),
         # if you need to scale the entry scale it's parent instead
 
-        self.entry = entry
         self.canvas = NodePath(self.guiItem.getCanvasNode())
-        self.entry.reparentTo(self.canvas)
         self.canvas.setPos(0,0,0)
 
-        self.entry.bind(DGG.CURSORMOVE,self.cursorMove)
+        self.entry = None
+        if entry is not None:
+            self.entry = entry
+            self.entry.reparentTo(self.canvas)
+            self.entry.bind(DGG.CURSORMOVE, self.cursorMove)
 
         self.canvas.node().setBounds(OmniBoundingVolume())
         self.canvas.node().setFinal(1)
         self.resetCanvas()
 
+    def setEntry(self, entry):
+        """
+        Sets a DirectEntry element for this scroll frame. A DirectEntryScroll
+        can only hold one entry at a time, so make sure to not call this
+        function twice or call clearEntry before to make sure no entry
+        is already set.
+        """
+        assert self.entry is None, "An entry was already set for this DirectEntryScroll element"
+        self.entry = entry
+        self.entry.reparentTo(self.canvas)
 
+        self.entry.bind(DGG.CURSORMOVE, self.cursorMove)
 
+    def clearEntry(self):
+        """
+        detaches and unbinds the entry from the scroll frame and its
+        events. You'll be responsible for destroying it.
+        """
+        if self.entry is None: return
+        self.entry.unbind(DGG.CURSORMOVE)
+        self.entry.detachNode()
+        self.entry = None
 
     def cursorMove(self, cursorX, cursorY):
         cursorX = self.entry.guiItem.getCursorX() * self.entry['text_scale'][0]

+ 3 - 4
direct/src/gui/DirectGuiBase.py

@@ -81,7 +81,6 @@ __all__ = ['DirectGuiBase', 'DirectGuiWidget']
 
 from panda3d.core import *
 from direct.showbase import ShowBaseGlobal
-from direct.showbase.ShowBase import ShowBase
 from . import DirectGuiGlobals as DGG
 from .OnscreenText import *
 from .OnscreenGeom import *
@@ -634,7 +633,7 @@ class DirectGuiBase(DirectObject.DirectObject):
         """
         # Need to tack on gui item specific id
         gEvent = event + self.guiId
-        if ShowBase.config.GetBool('debug-directgui-msgs', False):
+        if ShowBaseGlobal.config.GetBool('debug-directgui-msgs', False):
             from direct.showbase.PythonUtil import StackTrace
             print(gEvent)
             print(StackTrace())
@@ -663,7 +662,7 @@ class DirectGuiWidget(DirectGuiBase, NodePath):
     # Determine the default initial state for inactive (or
     # unclickable) components.  If we are in edit mode, these are
     # actually clickable by default.
-    guiEdit = ShowBase.config.GetBool('direct-gui-edit', False)
+    guiEdit = ShowBaseGlobal.config.GetBool('direct-gui-edit', False)
     if guiEdit:
         inactiveInitState = DGG.NORMAL
     else:
@@ -729,7 +728,7 @@ class DirectGuiWidget(DirectGuiBase, NodePath):
             guiObjectCollector.addLevel(1)
             guiObjectCollector.flushLevel()
             # track gui items by guiId for tracking down leaks
-            if ShowBase.config.GetBool('track-gui-items', False):
+            if ShowBaseGlobal.config.GetBool('track-gui-items', False):
                 if not hasattr(ShowBase, 'guiItems'):
                     ShowBase.guiItems = {}
                 if self.guiId in ShowBase.guiItems:

+ 5 - 4
direct/src/gui/DirectScrolledList.py

@@ -3,6 +3,7 @@
 __all__ = ['DirectScrolledListItem', 'DirectScrolledList']
 
 from panda3d.core import *
+from direct.showbase import ShowBaseGlobal
 from . import DirectGuiGlobals as DGG
 from direct.directnotify import DirectNotifyGlobal
 from direct.task.Task import Task
@@ -369,7 +370,7 @@ class DirectScrolledList(DirectFrame):
                 del self.currentSelected
             self["items"].remove(item)
             if type(item) != type(''):
-                item.reparentTo(hidden)
+                item.reparentTo(ShowBaseGlobal.hidden)
             self.refresh()
             return 1
         else:
@@ -387,7 +388,7 @@ class DirectScrolledList(DirectFrame):
                 item.destroy()
             self["items"].remove(item)
             if type(item) != type(''):
-                item.reparentTo(hidden)
+                item.reparentTo(ShowBaseGlobal.hidden)
             self.refresh()
             return 1
         else:
@@ -410,7 +411,7 @@ class DirectScrolledList(DirectFrame):
             self["items"].remove(item)
             if type(item) != type(''):
                 #RAU possible leak here, let's try to do the right thing
-                #item.reparentTo(hidden)
+                #item.reparentTo(ShowBaseGlobal.hidden)
                 item.removeNode()
             retval = 1
 
@@ -435,7 +436,7 @@ class DirectScrolledList(DirectFrame):
             self["items"].remove(item)
             if type(item) != type(''):
                 #RAU possible leak here, let's try to do the right thing
-                #item.reparentTo(hidden)
+                #item.reparentTo(ShowBaseGlobal.hidden)
                 item.removeNode()
             retval = 1
         if (refresh):

+ 199 - 62
direct/src/p3d/DeploymentTools.py

@@ -10,7 +10,7 @@ from direct.directnotify.DirectNotifyGlobal import *
 from direct.showbase.AppRunnerGlobal import appRunner
 from panda3d.core import PandaSystem, HTTPClient, Filename, VirtualFileSystem, Multifile
 from panda3d.core import TiXmlDocument, TiXmlDeclaration, TiXmlElement, readXmlStream
-from panda3d.core import PNMImage, PNMFileTypeRegistry
+from panda3d.core import PNMImage, PNMFileTypeRegistry, StringStream
 from direct.stdpy.file import *
 from direct.p3d.HostInfo import HostInfo
 # This is important for some reason
@@ -332,6 +332,135 @@ class Icon:
 
         return True
 
+    def generateMissingImages(self):
+        """ Generates image sizes that should be present but aren't by scaling
+        from the next higher size. """
+
+        for required_size in (256, 128, 48, 32, 16):
+            if required_size in self.images:
+                continue
+
+            sizes = sorted(self.images.keys())
+            if required_size * 2 in sizes:
+                from_size = required_size * 2
+            else:
+                for from_size in sizes:
+                    if from_size > required_size:
+                        break
+
+            if from_size > required_size:
+                Icon.notify.warning("Generating %dx%d icon by scaling down %dx%d image" % (required_size, required_size, from_size, from_size))
+
+                image = PNMImage(required_size, required_size)
+                if self.images[from_size].hasAlpha():
+                    image.addAlpha()
+                image.quickFilterFrom(self.images[from_size])
+                self.images[required_size] = image
+            else:
+                Icon.notify.warning("Cannot generate %dx%d icon; no higher resolution image available" % (required_size, required_size))
+
+    def _write_bitmap(self, fp, image, size, bpp):
+        """ Writes the bitmap header and data of an .ico file. """
+
+        fp.write(struct.pack('<IiiHHIIiiII', 40, size, size * 2, 1, bpp, 0, 0, 0, 0, 0, 0))
+
+        # XOR mask
+        if bpp == 24:
+            # Align rows to 4-byte boundary
+            rowalign = '\0' * (-(size * 3) & 3)
+            for y in xrange(size):
+                for x in xrange(size):
+                    r, g, b = image.getXel(x, size - y - 1)
+                    fp.write(struct.pack('<BBB', int(b * 255), int(g * 255), int(r * 255)))
+                fp.write(rowalign)
+
+        elif bpp == 32:
+            for y in xrange(size):
+                for x in xrange(size):
+                    r, g, b, a = image.getXelA(x, size - y - 1)
+                    fp.write(struct.pack('<BBBB', int(b * 255), int(g * 255), int(r * 255), int(a * 255)))
+
+        elif bpp == 8:
+            # We'll have to generate a palette of 256 colors.
+            hist = PNMImage.Histogram()
+            if image.hasAlpha():
+                # Make a copy without alpha channel.
+                image2 = PNMImage(image)
+                image2.premultiplyAlpha()
+                image2.removeAlpha()
+            else:
+                image2 = image
+            image2.make_histogram(hist)
+            colors = list(hist.get_pixels())
+            if len(colors) > 256:
+                # Palette too large; remove infrequent colors.
+                colors.sort(key=hist.get_count, reverse=True)
+
+                # Find the closest color on the palette matching each color
+                # that didn't fit.  This is certainly not the best palette
+                # generation code, but it'll do for now.
+                closest_indices = []
+                for color in colors[256:]:
+                    closest_index = 0
+                    closest_diff = 1025
+                    for i, closest_color in enumerate(colors[:256]):
+                        diff = abs(color.get_red() - closest_color.get_red()) \
+                             + abs(color.get_green() - closest_color.get_green()) \
+                             + abs(color.get_blue() - closest_color.get_blue())
+                        if diff < closest_diff:
+                            closest_index = i
+                            closest_diff = diff
+                    assert closest_diff < 100
+                    closest_indices.append(closest_index)
+
+            # Write the palette.
+            i = 0
+            while i < 256 and i < len(colors):
+                r, g, b, a = colors[i]
+                fp.write(struct.pack('<BBBB', b, g, r, 0))
+                i += 1
+            if i < 256:
+                # Fill the rest with zeroes.
+                fp.write(b'\x00' * (4 * (256 - i)))
+
+            # Write indices.  Align rows to 4-byte boundary.
+            rowalign = b'\0' * (-size & 3)
+            for y in xrange(size):
+                for x in xrange(size):
+                    pixel = image2.get_pixel(x, size - y - 1)
+                    index = colors.index(pixel)
+                    if index >= 256:
+                        # Find closest pixel instead.
+                        index = closest_indices[index - 256]
+                    fp.write(struct.pack('<B', index))
+                fp.write(rowalign)
+        else:
+            raise ValueError("Invalid bpp %d" % (bpp))
+
+        # Create an AND mask, aligned to 4-byte boundary
+        if image.hasAlpha() and bpp <= 8:
+            rowalign = b'\0' * (-((size + 7) >> 3) & 3)
+            for y in xrange(size):
+                mask = 0
+                num_bits = 7
+                for x in xrange(size):
+                    a = image.get_alpha_val(x, size - y - 1)
+                    if a <= 1:
+                        mask |= (1 << num_bits)
+                    num_bits -= 1
+                    if num_bits < 0:
+                        fp.write(struct.pack('<B', mask))
+                        mask = 0
+                        num_bits = 7
+                if num_bits < 7:
+                    fp.write(struct.pack('<B', mask))
+                fp.write(rowalign)
+        else:
+            andsize = (size + 7) >> 3
+            if andsize % 4 != 0:
+                andsize += 4 - (andsize % 4)
+            fp.write(b'\x00' * (andsize * size))
+
     def makeICO(self, fn):
         """ Writes the images to a Windows ICO file.  Returns True on success. """
 
@@ -339,57 +468,71 @@ class Icon:
             fn = Filename.fromOsSpecific(fn)
         fn.setBinary()
 
+        # ICO files only support resolutions up to 256x256.
         count = 0
         for size in self.images.keys():
+            if size < 256:
+                count += 1
             if size <= 256:
                 count += 1
+        dataoffs = 6 + count * 16
 
         ico = open(fn, 'wb')
         ico.write(struct.pack('<HHH', 0, 1, count))
 
-        # Write the directory
+        # Write 8-bpp image headers for sizes under 256x256.
         for size, image in self.images.items():
-            if size == 256:
+            if size >= 256:
+                continue
+            ico.write(struct.pack('<BB', size, size))
+
+            # Calculate row sizes
+            xorsize = size
+            if xorsize % 4 != 0:
+                xorsize += 4 - (xorsize % 4)
+            andsize = (size + 7) >> 3
+            if andsize % 4 != 0:
+                andsize += 4 - (andsize % 4)
+            datasize = 40 + 256 * 4 + (xorsize + andsize) * size
+
+            ico.write(struct.pack('<BBHHII', 0, 0, 1, 8, datasize, dataoffs))
+            dataoffs += datasize
+
+        # Write 24/32-bpp image headers.
+        for size, image in self.images.items():
+            if size > 256:
+                continue
+            elif size == 256:
                 ico.write('\0\0')
             else:
                 ico.write(struct.pack('<BB', size, size))
-            bpp = 32 if image.hasAlpha() else 24
-            ico.write(struct.pack('<BBHHII', 0, 0, 1, bpp, 0, 0))
 
-        # Now write the actual icons
-        ptr = 14
-        for size, image in self.images.items():
-            loc = ico.tell()
-            bpp = 32 if image.hasAlpha() else 24
-            ico.write(struct.pack('<IiiHHIIiiII', 40, size, size * 2, 1, bpp, 0, 0, 0, 0, 0, 0))
-
-            # XOR mask
-            if bpp == 24:
-                # Align rows to 4-byte boundary
-                rowalign = '\0' * (-(size * 3) & 3)
-                for y in xrange(size):
-                    for x in xrange(size):
-                        r, g, b = image.getXel(x, size - y - 1)
-                        ico.write(struct.pack('<BBB', int(b * 255), int(g * 255), int(r * 255)))
-                    ico.write(rowalign)
+            # Calculate the size so we can write the offset within the file.
+            if image.hasAlpha():
+                bpp = 32
+                xorsize = size * 4
             else:
-                for y in xrange(size):
-                    for x in xrange(size):
-                        r, g, b, a = image.getXelA(x, size - y - 1)
-                        ico.write(struct.pack('<BBBB', int(b * 255), int(g * 255), int(r * 255), int(a * 255)))
+                bpp = 24
+                xorsize = size * 3 + (-(size * 3) & 3)
+            andsize = (size + 7) >> 3
+            if andsize % 4 != 0:
+                andsize += 4 - (andsize % 4)
+            datasize = 40 + (xorsize + andsize) * size
+
+            ico.write(struct.pack('<BBHHII', 0, 0, 1, bpp, datasize, dataoffs))
+            dataoffs += datasize
 
-            # Empty AND mask, aligned to 4-byte boundary
-            #TODO: perhaps we should convert alpha into an AND mask
-            # to support older versions of Windows that don't support alpha.
-            ico.write('\0' * (size * (size / 8 + (-((size / 8) * 3) & 3))))
+        # Now write the actual icon bitmap data.
+        for size, image in self.images.items():
+            if size < 256:
+                self._write_bitmap(ico, image, size, 8)
 
-            # Go back to write the location
-            dataend = ico.tell()
-            ico.seek(ptr)
-            ico.write(struct.pack('<II', dataend - loc, loc))
-            ico.seek(dataend)
-            ptr += 16
+        for size, image in self.images.items():
+            if size <= 256:
+                bpp = 32 if image.hasAlpha() else 24
+                self._write_bitmap(ico, image, size, bpp)
 
+        assert ico.tell() == dataoffs
         ico.close()
 
         return True
@@ -401,32 +544,35 @@ class Icon:
             fn = Filename.fromOsSpecific(fn)
         fn.setBinary()
 
-        vfs = VirtualFileSystem.getGlobalPtr()
-        stream = vfs.openWriteFile(fn, False, True)
-        icns = open(stream, 'wb')
+        icns = open(fn, 'wb')
         icns.write(b'icns\0\0\0\0')
 
-        icon_types = {16: 'is32', 32: 'il32', 48: 'ih32', 128: 'it32'}
-        mask_types = {16: 's8mk', 32: 'l8mk', 48: 'h8mk', 128: 't8mk'}
-        png_types = {256: 'ic08', 512: 'ic09'}
+        icon_types = {16: b'is32', 32: b'il32', 48: b'ih32', 128: b'it32'}
+        mask_types = {16: b's8mk', 32: b'l8mk', 48: b'h8mk', 128: b't8mk'}
+        png_types = {256: b'ic08', 512: b'ic09', 1024: b'ic10'}
 
         pngtype = PNMFileTypeRegistry.getGlobalPtr().getTypeFromExtension("png")
 
-        for size, image in self.images.items():
-            if size in png_types:
-                if pngtype is None:
-                    continue
-                icns.write(png_types[size])
-                icns.write(b'\0\0\0\0')
-                start = icns.tell()
-
+        for size, image in sorted(self.images.items(), key=lambda item:item[0]):
+            if size in png_types and pngtype is not None:
+                stream = StringStream()
                 image.write(stream, "", pngtype)
-                pngsize = icns.tell() - start
-                icns.seek(start - 4)
-                icns.write(struct.pack('>I', pngsize + 8))
-                icns.seek(start + pngsize)
+                pngdata = stream.data
+
+                icns.write(png_types[size])
+                icns.write(struct.pack('>I', len(pngdata)))
+                icns.write(pngdata)
 
             elif size in icon_types:
+                # If it has an alpha channel, we write out a mask too.
+                if image.hasAlpha():
+                    icns.write(mask_types[size])
+                    icns.write(struct.pack('>I', size * size + 8))
+
+                    for y in xrange(size):
+                        for x in xrange(size):
+                            icns.write(struct.pack('<B', int(image.getAlpha(x, y) * 255)))
+
                 icns.write(icon_types[size])
                 icns.write(struct.pack('>I', size * size * 4 + 8))
 
@@ -435,15 +581,6 @@ class Icon:
                         r, g, b = image.getXel(x, y)
                         icns.write(struct.pack('>BBBB', 0, int(r * 255), int(g * 255), int(b * 255)))
 
-                if not image.hasAlpha():
-                    continue
-                icns.write(mask_types[size])
-                icns.write(struct.pack('>I', size * size + 8))
-
-                for y in xrange(size):
-                    for x in xrange(size):
-                        icns.write(struct.pack('<B', int(image.getAlpha(x, y) * 255)))
-
         length = icns.tell()
         icns.seek(4)
         icns.write(struct.pack('>I', length))

+ 4 - 1
direct/src/showbase/ShowBase.py

@@ -207,7 +207,10 @@ class ShowBase(DirectObject.DirectObject):
             # Has the clusterSyncFlag been set via a config variable
             self.clusterSyncFlag = self.config.GetBool('cluster-sync', 0)
 
-        self.hidden = NodePath('hidden')
+        # We've already created aspect2d in ShowBaseGlobal, for the
+        # benefit of creating DirectGui elements before ShowBase.
+        from . import ShowBaseGlobal
+        self.hidden = ShowBaseGlobal.hidden
 
         ## The global graphics engine, ie. GraphicsEngine.getGlobalPtr()
         self.graphicsEngine = GraphicsEngine.getGlobalPtr()

+ 1 - 0
direct/src/showbase/ShowBaseGlobal.py

@@ -25,6 +25,7 @@ pandaSystem = PandaSystem.getGlobalPtr()
 
 # This is defined here so GUI elements can be instantiated before ShowBase.
 aspect2d = NodePath(PGTop("aspect2d"))
+hidden = NodePath("hidden")
 
 # Set direct notify categories now that we have config
 directNotify.setDconfigLevels()

+ 2 - 1
direct/src/showbase/Transitions.py

@@ -5,6 +5,7 @@ a particular color."""
 __all__ = ['Transitions']
 
 from panda3d.core import *
+from direct.showbase import ShowBaseGlobal
 from direct.gui.DirectGui import DirectFrame
 from direct.gui import DirectGuiGlobals as DGG
 from direct.interval.LerpInterval import LerpColorScaleInterval, LerpColorInterval, LerpScaleInterval, LerpPosInterval
@@ -73,7 +74,7 @@ class Transitions:
             # so that it will also obscure mouse events for objects
             # positioned behind it.
             self.fade = DirectFrame(
-                parent = hidden,
+                parent = ShowBaseGlobal.hidden,
                 guiId = 'fade',
                 relief = None,
                 image = self.fadeModel,

+ 2 - 1
direct/src/tkpanels/MopathRecorder.py

@@ -4,6 +4,7 @@ __all__ = ['MopathRecorder']
 
 # Import Tkinter, Pmw, and the dial code from this directory tree.
 from panda3d.core import *
+from direct.showbase import ShowBaseGlobal
 from direct.showbase.DirectObject import DirectObject
 from direct.showbase.TkGlobal import *
 from direct.tkwidgets.AppShell import *
@@ -852,7 +853,7 @@ class MopathRecorder(AppShell, DirectObject):
         if self.getVariable('Style', 'Marker').get():
             self.playbackMarker.reparentTo(self.recorderNodePath)
         else:
-            self.playbackMarker.reparentTo(hidden)
+            self.playbackMarker.reparentTo(ShowBaseGlobal.hidden)
 
     def setNumSegs(self, value):
         self.numSegs = int(value)

+ 38 - 0
doc/ReleaseNotes

@@ -1,3 +1,41 @@
+------------------------  RELEASE 1.10.4  -----------------------
+
+This release fixes a regression with DirectScrolledList in 1.10.3,
+fixes various other bugs, and introduces a few minor features.
+
+* Fix exception trying to create DirectScrolledList
+* Fix flickering in DirectScrolledFrame and other scissor issues (#681)
+* Experimental support for Python 3.8
+* Support adding icons to deployed applications
+* Support non-affine (eg. projective) transforms in calc_tight_bounds
+* Allow setting notify-output after initial import
+* Fix macOS issue locating Panda3D using Python 2.7.13+ from python.org
+* Support for Maya 2019
+* On Windows, pip is now installed by the installer (#690)
+* Fix Actor.makeSubpart on models with pre-bound animations (#647)
+* Properly interrupt task manager if first task chain raises error (#692)
+* Fix return value of encrypt_string in Python 3 (#684)
+* Support writing loader plug-ins in Python
+* Fix reading multiple p3d_TextureMatrix[] values from GLSL shaders
+* Fix shader error flag not being set if GLSL compilation failed (#622)
+* Add NodePath.replace_texture() convenience method
+* Fix deadlock when building with SIMPLE_THREADS=1 (#704)
+* Fix DirectOptionMenu cancelFrame not working inside scrolled frame (#658)
+* Fix assertion when calling analyze() on geometry with strip cut index
+* Implement fallback in GL renderer when F_sluminance is not supported (#693)
+* Set reasonable limits for sliders in ParticlePanel
+* Fix for DirectEntry autoCapitalize feature on Python 3 (#628)
+* Fix various DirectGUI items not working before ShowBase is instantiated
+* Work around an MSVC compiler bug in the release build
+* PythonUtil.weightedChoice now raises IndexError on empty list
+* Support changing DirectScrollBar width after initialiation (#699)
+* Workaround for Bullet deadlock when adding shape to a scaled body (#689)
+* Support setting DirectEntryScroll entry after initialization (#702)
+* Fix some missing imports in directtools (#698)
+* Fix undefined behavior issue when using musl-libc
+* Update Eigen in Windows thirdparty packages to 3.3.7
+* Update metadata of pip wheels
+
 ------------------------  RELEASE 1.10.3  -----------------------
 
 This is another bugfix release that addresses a variety of issues

+ 10 - 1
dtool/src/interrogate/interfaceMakerPythonNative.cxx

@@ -2878,8 +2878,13 @@ write_module_class(ostream &out, Object *obj) {
 
   // destructor tp_dealloc;
   out << "    &Dtool_FreeInstance_" << ClassName << ",\n";
-  // printfunc tp_print;
+
+  out << "#if PY_VERSION_HEX >= 0x03080000\n";
+  out << "    0, // tp_vectorcall_offset\n";
+  out << "#else\n";
   write_function_slot(out, 4, slots, "tp_print");
+  out << "#endif\n";
+
   // getattrfunc tp_getattr;
   write_function_slot(out, 4, slots, "tp_getattr");
   // setattrfunc tp_setattr;
@@ -3070,6 +3075,10 @@ write_module_class(ostream &out, Object *obj) {
   out << "#if PY_VERSION_HEX >= 0x03040000\n";
   out << "    nullptr, // tp_finalize\n";
   out << "#endif\n";
+  // vectorcallfunc tp_vectorcall
+  out << "#if PY_VERSION_HEX >= 0x03080000\n";
+  out << "    nullptr, // tp_vectorcall\n";
+  out << "#endif\n";
   out << "  },\n";
 
   // It's tempting to initialize the type handle here, but this causes static

+ 8 - 1
dtool/src/interrogatedb/dtool_super_base.cxx

@@ -80,7 +80,7 @@ Dtool_PyTypedObject *Dtool_GetSuperBase() {
       sizeof(Dtool_PyInstDef),
       0, // tp_itemsize
       &Dtool_FreeInstance_DTOOL_SUPER_BASE,
-      nullptr, // tp_print
+      0, // tp_vectorcall_offset
       nullptr, // tp_getattr
       nullptr, // tp_setattr
 #if PY_MAJOR_VERSION >= 3
@@ -129,6 +129,13 @@ Dtool_PyTypedObject *Dtool_GetSuperBase() {
       nullptr, // tp_subclasses
       nullptr, // tp_weaklist
       nullptr, // tp_del
+      0, // tp_version_tag,
+#if PY_VERSION_HEX >= 0x03040000
+      nullptr, // tp_finalize
+#endif
+#if PY_VERSION_HEX >= 0x03080000
+      nullptr, // tp_vectorcall
+#endif
     },
     TypeHandle::none(),
     Dtool_PyModuleClassInit_DTOOL_SUPER_BASE,

+ 1 - 1
dtool/src/interrogatedb/py_compat.h

@@ -139,7 +139,7 @@ typedef long Py_hash_t;
 
 /* Python 3.6 */
 
-#ifndef _PyObject_CallNoArg
+#if PY_VERSION_HEX < 0x03080000 && !defined(_PyObject_CallNoArg)
 INLINE PyObject *_PyObject_CallNoArg(PyObject *func) {
   static PyTupleObject empty_tuple = {PyVarObject_HEAD_INIT(nullptr, 0)};
 #ifdef Py_TRACE_REFS

+ 73 - 10
dtool/src/interrogatedb/py_wrappers.cxx

@@ -541,7 +541,7 @@ static PyObject *Dtool_MappingWrapper_keys(PyObject *self, PyObject *) {
     sizeof(Dtool_SequenceWrapper),
     0, // tp_itemsize
     Dtool_WrapperBase_dealloc,
-    nullptr, // tp_print
+    0, // tp_vectorcall_offset
     nullptr, // tp_getattr
     nullptr, // tp_setattr
     nullptr, // tp_compare
@@ -582,6 +582,13 @@ static PyObject *Dtool_MappingWrapper_keys(PyObject *self, PyObject *) {
     nullptr, // tp_subclasses
     nullptr, // tp_weaklist
     nullptr, // tp_del
+    0, // tp_version_tag,
+#if PY_VERSION_HEX >= 0x03040000
+    nullptr, // tp_finalize
+#endif
+#if PY_VERSION_HEX >= 0x03080000
+    nullptr, // tp_vectorcall
+#endif
   };
 
   static bool registered = false;
@@ -675,7 +682,7 @@ static PyObject *Dtool_MappingWrapper_values(PyObject *self, PyObject *) {
     sizeof(Dtool_MappingWrapper),
     0, // tp_itemsize
     Dtool_WrapperBase_dealloc,
-    nullptr, // tp_print
+    0, // tp_vectorcall_offset
     nullptr, // tp_getattr
     nullptr, // tp_setattr
     nullptr, // tp_compare
@@ -716,6 +723,13 @@ static PyObject *Dtool_MappingWrapper_values(PyObject *self, PyObject *) {
     nullptr, // tp_subclasses
     nullptr, // tp_weaklist
     nullptr, // tp_del
+    0, // tp_version_tag,
+#if PY_VERSION_HEX >= 0x03040000
+    nullptr, // tp_finalize
+#endif
+#if PY_VERSION_HEX >= 0x03080000
+    nullptr, // tp_vectorcall
+#endif
   };
 
   static bool registered = false;
@@ -817,7 +831,7 @@ static PyObject *Dtool_MappingWrapper_items(PyObject *self, PyObject *) {
     sizeof(Dtool_MappingWrapper),
     0, // tp_itemsize
     Dtool_WrapperBase_dealloc,
-    nullptr, // tp_print
+    0, // tp_vectorcall_offset
     nullptr, // tp_getattr
     nullptr, // tp_setattr
     nullptr, // tp_compare
@@ -858,6 +872,13 @@ static PyObject *Dtool_MappingWrapper_items(PyObject *self, PyObject *) {
     nullptr, // tp_subclasses
     nullptr, // tp_weaklist
     nullptr, // tp_del
+    0, // tp_version_tag,
+#if PY_VERSION_HEX >= 0x03040000
+    nullptr, // tp_finalize
+#endif
+#if PY_VERSION_HEX >= 0x03080000
+    nullptr, // tp_vectorcall
+#endif
   };
 
   static bool registered = false;
@@ -1192,7 +1213,7 @@ Dtool_SequenceWrapper *Dtool_NewSequenceWrapper(PyObject *self, const char *name
     sizeof(Dtool_SequenceWrapper),
     0, // tp_itemsize
     Dtool_WrapperBase_dealloc,
-    nullptr, // tp_print
+    0, // tp_vectorcall_offset
     nullptr, // tp_getattr
     nullptr, // tp_setattr
     nullptr, // tp_compare
@@ -1233,6 +1254,13 @@ Dtool_SequenceWrapper *Dtool_NewSequenceWrapper(PyObject *self, const char *name
     nullptr, // tp_subclasses
     nullptr, // tp_weaklist
     nullptr, // tp_del
+    0, // tp_version_tag,
+#if PY_VERSION_HEX >= 0x03040000
+    nullptr, // tp_finalize
+#endif
+#if PY_VERSION_HEX >= 0x03080000
+    nullptr, // tp_vectorcall
+#endif
   };
 
   static bool registered = false;
@@ -1296,7 +1324,7 @@ Dtool_MutableSequenceWrapper *Dtool_NewMutableSequenceWrapper(PyObject *self, co
     sizeof(Dtool_MutableSequenceWrapper),
     0, // tp_itemsize
     Dtool_WrapperBase_dealloc,
-    nullptr, // tp_print
+    0, // tp_vectorcall_offset
     nullptr, // tp_getattr
     nullptr, // tp_setattr
     nullptr, // tp_compare
@@ -1337,6 +1365,13 @@ Dtool_MutableSequenceWrapper *Dtool_NewMutableSequenceWrapper(PyObject *self, co
     nullptr, // tp_subclasses
     nullptr, // tp_weaklist
     nullptr, // tp_del
+    0, // tp_version_tag,
+#if PY_VERSION_HEX >= 0x03040000
+    nullptr, // tp_finalize
+#endif
+#if PY_VERSION_HEX >= 0x03080000
+    nullptr, // tp_vectorcall
+#endif
   };
 
   static bool registered = false;
@@ -1404,7 +1439,7 @@ Dtool_MappingWrapper *Dtool_NewMappingWrapper(PyObject *self, const char *name)
     sizeof(Dtool_MappingWrapper),
     0, // tp_itemsize
     Dtool_WrapperBase_dealloc,
-    nullptr, // tp_print
+    0, // tp_vectorcall_offset
     nullptr, // tp_getattr
     nullptr, // tp_setattr
     nullptr, // tp_compare
@@ -1445,6 +1480,13 @@ Dtool_MappingWrapper *Dtool_NewMappingWrapper(PyObject *self, const char *name)
     nullptr, // tp_subclasses
     nullptr, // tp_weaklist
     nullptr, // tp_del
+    0, // tp_version_tag,
+#if PY_VERSION_HEX >= 0x03040000
+    nullptr, // tp_finalize
+#endif
+#if PY_VERSION_HEX >= 0x03080000
+    nullptr, // tp_vectorcall
+#endif
   };
 
   static bool registered = false;
@@ -1517,7 +1559,7 @@ Dtool_MappingWrapper *Dtool_NewMutableMappingWrapper(PyObject *self, const char
     sizeof(Dtool_MappingWrapper),
     0, // tp_itemsize
     Dtool_WrapperBase_dealloc,
-    nullptr, // tp_print
+    0, // tp_vectorcall_offset
     nullptr, // tp_getattr
     nullptr, // tp_setattr
     nullptr, // tp_compare
@@ -1558,6 +1600,13 @@ Dtool_MappingWrapper *Dtool_NewMutableMappingWrapper(PyObject *self, const char
     nullptr, // tp_subclasses
     nullptr, // tp_weaklist
     nullptr, // tp_del
+    0, // tp_version_tag,
+#if PY_VERSION_HEX >= 0x03040000
+    nullptr, // tp_finalize
+#endif
+#if PY_VERSION_HEX >= 0x03080000
+    nullptr, // tp_vectorcall
+#endif
   };
 
   static bool registered = false;
@@ -1594,7 +1643,7 @@ Dtool_NewGenerator(PyObject *self, iternextfunc gen_next) {
     sizeof(Dtool_GeneratorWrapper),
     0, // tp_itemsize
     Dtool_WrapperBase_dealloc,
-    nullptr, // tp_print
+    0, // tp_vectorcall_offset
     nullptr, // tp_getattr
     nullptr, // tp_setattr
     nullptr, // tp_compare
@@ -1635,6 +1684,13 @@ Dtool_NewGenerator(PyObject *self, iternextfunc gen_next) {
     nullptr, // tp_subclasses
     nullptr, // tp_weaklist
     nullptr, // tp_del
+    0, // tp_version_tag,
+#if PY_VERSION_HEX >= 0x03040000
+    nullptr, // tp_finalize
+#endif
+#if PY_VERSION_HEX >= 0x03080000
+    nullptr, // tp_vectorcall
+#endif
   };
 
   if (PyType_Ready(&wrapper_type) < 0) {
@@ -1663,7 +1719,7 @@ Dtool_NewStaticProperty(PyTypeObject *type, const PyGetSetDef *getset) {
     sizeof(PyGetSetDescrObject),
     0, // tp_itemsize
     (destructor)Dtool_StaticProperty_dealloc,
-    nullptr, // tp_print
+    0, // tp_vectorcall_offset
     nullptr, // tp_getattr
     nullptr, // tp_setattr
     nullptr, // tp_reserved
@@ -1696,7 +1752,7 @@ Dtool_NewStaticProperty(PyTypeObject *type, const PyGetSetDef *getset) {
     nullptr, // tp_init
     nullptr, // tp_alloc
     nullptr, // tp_new
-    nullptr, // tp_del
+    nullptr, // tp_free
     nullptr, // tp_is_gc
     nullptr, // tp_bases
     nullptr, // tp_mro
@@ -1704,6 +1760,13 @@ Dtool_NewStaticProperty(PyTypeObject *type, const PyGetSetDef *getset) {
     nullptr, // tp_subclasses
     nullptr, // tp_weaklist
     nullptr, // tp_del
+    0, // tp_version_tag,
+#if PY_VERSION_HEX >= 0x03040000
+    nullptr, // tp_finalize
+#endif
+#if PY_VERSION_HEX >= 0x03080000
+    nullptr, // tp_vectorcall
+#endif
   };
 
   if (PyType_Ready(&wrapper_type) < 0) {

+ 4 - 1
dtool/src/prc/notify.cxx

@@ -19,9 +19,12 @@
 #include "filename.h"
 #include "config_prc.h"
 
-#include <atomic>
 #include <ctype.h>
 
+#ifdef PHAVE_ATOMIC
+#include <atomic>
+#endif
+
 #ifdef BUILD_IPHONE
 #include <fcntl.h>
 #endif

+ 27 - 0
makepanda/installer.nsi

@@ -84,6 +84,7 @@ LangString DESC_SecTools ${LANG_ENGLISH} "Useful tools and model converters to h
 LangString DESC_SecGroupPython ${LANG_ENGLISH} "Contains modules that provide Python support for Panda3D."
 LangString DESC_SecPyShared ${LANG_ENGLISH} "Contains the common Python code used by the Panda3D Python bindings."
 LangString DESC_SecPython ${LANG_ENGLISH} "Contains a ${REGVIEW}-bit copy of Python ${INCLUDE_PYVER} preconfigured to make use of Panda3D."
+LangString DESC_SecEnsurePip ${LANG_ENGLISH} "Installs the pip package manager into the included Python installation."
 LangString DESC_SecHeadersLibs ${LANG_ENGLISH} "Headers and libraries needed for C++ development with Panda3D."
 LangString DESC_SecSamples ${LANG_ENGLISH} "The sample programs demonstrate how to make Python applications with Panda3D."
 LangString DESC_SecMaxPlugins ${LANG_ENGLISH} "Plug-ins for Autodesk 3ds Max (${REGVIEW}-bit) that can be used to export models to Panda3D."
@@ -477,6 +478,19 @@ Section "Python ${INCLUDE_PYVER}" SecPython
     SkipRegPath:
 
 SectionEnd
+
+Section "Install pip" SecEnsurePip
+    SectionIn 1 2 3
+
+    SetDetailsPrint both
+    DetailPrint "Installing the pip package manager..."
+    SetDetailsPrint listonly
+
+    SetOutPath $INSTDIR
+    nsExec::ExecToLog '"$INSTDIR\python\python.exe" -m ensurepip --default-pip'
+    Pop $0
+    DetailPrint "Command returned exit status $0"
+SectionEnd
 !endif
 
 !macro MaybeEnablePyBindingSection PYVER
@@ -537,6 +551,18 @@ Function .onSelChange
         IntOp $R0 $R0 | ${SF_SELECTED}
         SectionSetFlags ${SecPyShared} $R0
     ${EndIf}
+
+    !ifdef INCLUDE_PYVER
+        ${If} ${SectionIsSelected} ${SecPython}
+            !insertmacro SectionFlagIsSet ${SecEnsurePip} ${SF_RO} 0 SkipSelectEnsurePip
+            !insertmacro SelectSection ${SecEnsurePip}
+            SkipSelectEnsurePip:
+            !insertmacro ClearSectionFlag ${SecEnsurePip} ${SF_RO}
+        ${Else}
+            !insertmacro UnselectSection ${SecEnsurePip}
+            !insertmacro SetSectionFlag ${SecEnsurePip} ${SF_RO}
+        ${EndIf}
+    !endif
 FunctionEnd
 
 !ifdef INCLUDE_PYVER
@@ -917,6 +943,7 @@ SectionEnd
   !endif
   !ifdef INCLUDE_PYVER
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPython} $(DESC_SecPython)
+    !insertmacro MUI_DESCRIPTION_TEXT ${SecEnsurePip} $(DESC_SecEnsurePip)
   !endif
   !insertmacro MUI_DESCRIPTION_TEXT ${SecHeadersLibs} $(DESC_SecHeadersLibs)
   !ifdef HAVE_SAMPLES

+ 3 - 3
makepanda/makepackage.py

@@ -562,9 +562,9 @@ def MakeInstallerOSX(version, runtime=False, python_versions=[], **kwargs):
         oscmd("mkdir -p dstroot/pybindings%s/Library/Python/%s/site-packages" % (pyver, pyver))
         WriteFile("dstroot/pybindings%s/Library/Python/%s/site-packages/Panda3D.pth" % (pyver, pyver), "/Developer/Panda3D")
 
-        # Evidently not all Python 3.7 installations read the above path, so do this for now
-        # See GitHub #502
-        if pyver == "3.7":
+        # Somewhere in Python 2.7.13 and 3.7, the above path was removed from
+        # sys.path of the python.org distribution.  See bpo-28440 and GH #502.
+        if pyver not in ("3.0", "3.1", "3.2", "3.3", "3.4", "3.5", "3.6"):
             dir = "dstroot/pybindings%s/Library/Frameworks/Python.framework/Versions/%s/lib/python%s/site-packages" % (pyver, pyver, pyver)
             oscmd("mkdir -p %s" % (dir))
             WriteFile("%s/Panda3D.pth" % (dir), "/Developer/Panda3D")

+ 4 - 3
makepanda/makepanda.py

@@ -2903,7 +2903,9 @@ if '__file__' in locals():
 
     bindir = os.path.join(os.path.dirname(__file__), '..', 'bin')
     if os.path.isdir(bindir):
-        if not os.environ.get('PATH'):
+        if hasattr(os, 'add_dll_directory'):
+            os.add_dll_directory(bindir)
+        elif not os.environ.get('PATH'):
             os.environ['PATH'] = bindir
         else:
             os.environ['PATH'] = bindir + os.pathsep + os.environ['PATH']
@@ -3210,9 +3212,8 @@ if tp_dir is not None:
 
 # Copy over the MSVC runtime.
 if GetTarget() == 'windows' and "VISUALSTUDIO" in SDK:
-    vsver = "%s%s" % SDK["VISUALSTUDIO_VERSION"]
     vcver = "%s%s" % (SDK["MSVC_VERSION"][0], 0)        # ignore minor version.
-    crtname = "Microsoft.VC%s.CRT" % (vsver)
+    crtname = "Microsoft.VC%s.CRT" % (vcver)
     if ("VCTOOLSVERSION" in SDK):
         dir = os.path.join(SDK["VISUALSTUDIO"], "VC", "Redist", "MSVC", SDK["VCTOOLSVERSION"], "onecore", GetTargetArch(), crtname)
     else:

+ 5 - 1
makepanda/makepandacore.py

@@ -105,6 +105,7 @@ MAYAVERSIONINFO = [("MAYA6",   "6.0"),
                    ("MAYA20165","2016.5"),
                    ("MAYA2017","2017"),
                    ("MAYA2018","2018"),
+                   ("MAYA2019","2019"),
 ]
 
 MAXVERSIONINFO = [("MAX6", "SOFTWARE\\Autodesk\\3DSMAX\\6.0", "installdir", "maxsdk\\cssdk\\include"),
@@ -2788,7 +2789,7 @@ def SetupVisualStudioEnviron():
 
     # Targeting the 7.1 SDK (which is the only way to have Windows XP support)
     # with Visual Studio 2015 requires use of the Universal CRT.
-    if winsdk_ver == '7.1' and SDK["VISUALSTUDIO_VERSION"] >= (14,0):
+    if winsdk_ver in ('7.1', '7.1A') and SDK["VISUALSTUDIO_VERSION"] >= (14,0):
         win_kit = GetRegistryKey("SOFTWARE\\Microsoft\\Windows Kits\\Installed Roots", "KitsRoot10")
 
         # Fallback in case we can't read the registry.
@@ -3392,6 +3393,9 @@ def GetPythonABI():
 
     soabi = 'cpython-%d%d' % (sys.version_info[:2])
 
+    if sys.version_info >= (3, 8):
+        return soabi
+
     debug_flag = sysconfig.get_config_var('Py_DEBUG')
     if (debug_flag is None and hasattr(sys, 'gettotalrefcount')) or debug_flag:
         soabi += 'd'

+ 8 - 2
makepanda/makewheel.py

@@ -29,6 +29,9 @@ def get_abi_tag():
 
     soabi = 'cp%d%d' % (sys.version_info[:2])
 
+    if sys.version_info >= (3, 8):
+        return soabi
+
     debug_flag = get_config_var('Py_DEBUG')
     if (debug_flag is None and hasattr(sys, 'gettotalrefcount')) or debug_flag:
         soabi += 'd'
@@ -168,15 +171,18 @@ questions.
 PANDA3D_TOOLS_INIT = """import os, sys
 import panda3d
 
+dir = os.path.dirname(panda3d.__file__)
+del panda3d
+
 if sys.platform in ('win32', 'cygwin'):
     path_var = 'PATH'
+    if hasattr(os, 'add_dll_directory'):
+        os.add_dll_directory(dir)
 elif sys.platform == 'darwin':
     path_var = 'DYLD_LIBRARY_PATH'
 else:
     path_var = 'LD_LIBRARY_PATH'
 
-dir = os.path.dirname(panda3d.__file__)
-del panda3d
 if not os.environ.get(path_var):
     os.environ[path_var] = dir
 else:

+ 8 - 3
makepanda/test_wheel.py

@@ -19,9 +19,6 @@ def test_wheel(wheel, verbose=False):
     print("Setting up virtual environment in {0}".format(envdir))
     sys.stdout.flush()
 
-    # Make sure pip is up-to-date first.
-    subprocess.call([sys.executable, "-B", "-m", "pip", "install", "-U", "pip"])
-
     # Create a virtualenv.
     if sys.version_info >= (3, 0):
         subprocess.call([sys.executable, "-B", "-m", "venv", "--clear", envdir])
@@ -39,6 +36,14 @@ def test_wheel(wheel, verbose=False):
         shutil.rmtree(envdir)
         sys.exit(1)
 
+    # Temp hack to patch issue pypa/pip#6885 in pip 19.2.2 and Python 3.8.
+    if sys.platform == "win32" and "-cp38-cp38-" in wheel and os.path.isdir(os.path.join(envdir, "Lib", "site-packages", "pip-19.2.2.dist-info")):
+        pep425tags = os.path.join(envdir, "Lib", "site-packages", "pip", "_internal", "pep425tags.py")
+        if os.path.isfile(pep425tags):
+            data = open(pep425tags, "r").read()
+            data = data.replace(" m = 'm'\n", " m = ''\n")
+            open(pep425tags, "w").write(data)
+
     # Install pytest into the environment, as well as our wheel.
     if subprocess.call([python, "-m", "pip", "install", "pytest", wheel]) != 0:
         shutil.rmtree(envdir)

+ 18 - 4
panda/src/bullet/bulletBodyNode.cxx

@@ -353,9 +353,17 @@ do_add_shape(BulletShape *bullet_shape, const TransformState *ts) {
 
   // Reset the shape scaling before we add a shape, and remember the current
   // Scale so we can restore it later...
-  NodePath np = NodePath::any_path((PandaNode *)this);
-  LVector3 scale = np.get_scale();
-  np.set_scale(1.0);
+  CPT(TransformState) prev_transform = get_transform();
+  bool scale_changed = false;
+  if (!prev_transform->is_identity() && prev_transform->get_scale() != LVecBase3(1.0, 1.0, 1.0)) {
+    // As a hack, temporarily release the lock, since transform_changed will
+    // otherwise deadlock trying to grab it again.  See GitHub issue #689.
+    LightMutex &lock = BulletWorld::get_global_lock();
+    lock.release();
+    set_transform(prev_transform->set_scale(LVecBase3(1.0, 1.0, 1.0)));
+    lock.acquire();
+    scale_changed = true;
+  }
 
   // Root shape
   btCollisionShape *previous = get_object()->getCollisionShape();
@@ -417,7 +425,13 @@ do_add_shape(BulletShape *bullet_shape, const TransformState *ts) {
   _shapes.push_back(bullet_shape);
 
   // Restore the local scaling again
-  np.set_scale(scale);
+  if (scale_changed) {
+    CPT(TransformState) transform = get_transform()->set_scale(prev_transform->get_scale());
+    LightMutex &lock = BulletWorld::get_global_lock();
+    lock.release();
+    set_transform(std::move(transform));
+    lock.acquire();
+  }
 
   do_shape_changed();
 }

+ 6 - 3
panda/src/bullet/bulletContactCallbacks.h

@@ -61,7 +61,9 @@ contact_added_callback(btManifoldPoint &cp,
     PT(PandaNode) node1 = (PandaNode *)obj1->getUserPointer();
 #endif
 
-    bullet_cat.debug() << "contact added: " << cp.m_userPersistentData << std::endl;
+    if (bullet_cat.is_debug()) {
+      bullet_cat.debug() << "contact added: " << cp.m_userPersistentData << std::endl;
+    }
 
     // Gather persistent data
     UserPersistentData *data = new UserPersistentData();
@@ -123,8 +125,9 @@ contact_processed_callback(btManifoldPoint &cp,
  */
 static bool
 contact_destroyed_callback(void *userPersistentData) {
-
-  bullet_cat.debug() << "contact removed: " << userPersistentData << std::endl;
+  if (bullet_cat.is_debug()) {
+    bullet_cat.debug() << "contact removed: " << userPersistentData << std::endl;
+  }
 
   UserPersistentData *data = (UserPersistentData *)userPersistentData;
 

+ 6 - 4
panda/src/bullet/bulletDebugNode.cxx

@@ -384,8 +384,9 @@ drawTriangle(const btVector3 &v0, const btVector3 &v1, const btVector3 &v2, cons
  */
 void BulletDebugNode::DebugDraw::
 drawTriangle(const btVector3 &v0, const btVector3 &v1, const btVector3 &v2, const btVector3 &n0, const btVector3 &n1, const btVector3 &n2, const btVector3 &color, btScalar alpha) {
-
-  bullet_cat.debug() << "drawTriangle(2) - not yet implemented!" << std::endl;
+  if (bullet_cat.is_debug()) {
+    bullet_cat.debug() << "drawTriangle(2) - not yet implemented!" << std::endl;
+  }
 }
 
 /**
@@ -405,8 +406,9 @@ drawContactPoint(const btVector3 &point, const btVector3 &normal, btScalar dista
  */
 void BulletDebugNode::DebugDraw::
 draw3dText(const btVector3 &location, const char *text) {
-
-  bullet_cat.debug() << "draw3dText - not yet implemented!" << std::endl;
+  if (bullet_cat.is_debug()) {
+    bullet_cat.debug() << "draw3dText - not yet implemented!" << std::endl;
+  }
 }
 
 /**

+ 3 - 1
panda/src/bullet/config_bullet.cxx

@@ -207,7 +207,9 @@ init_libbullet() {
 
   // Initialize notification category
   bullet_cat.init();
-  bullet_cat.debug() << "initialize module" << std::endl;
+  if (bullet_cat.is_debug()) {
+    bullet_cat.debug() << "initialize module" << std::endl;
+  }
 
   // Register the Bullet system
   PandaSystem *ps = PandaSystem::get_global_ptr();

+ 0 - 6
panda/src/linmath/lmatrix3_src.I

@@ -132,11 +132,6 @@ FLOATNAME(LMatrix3)(const FLOATNAME(LVecBase3) &row0,
                     const FLOATNAME(LVecBase3) &row2) {
   TAU_PROFILE("LMatrix3::LMatrix3(const LVecBase3 &, ...)", " ", TAU_USER);
 
-#ifdef HAVE_EIGEN
-  _m.row(0) = row0._v;
-  _m.row(1) = row1._v;
-  _m.row(2) = row2._v;
-#else
   _m(0, 0) = row0._v(0);
   _m(0, 1) = row0._v(1);
   _m(0, 2) = row0._v(2);
@@ -146,7 +141,6 @@ FLOATNAME(LMatrix3)(const FLOATNAME(LVecBase3) &row0,
   _m(2, 0) = row2._v(0);
   _m(2, 1) = row2._v(1);
   _m(2, 2) = row2._v(2);
-#endif  // HAVE_EIGEN
 }
 
 /**

+ 6 - 2
panda/src/ode/odeGeom.cxx

@@ -33,12 +33,16 @@ TypeHandle OdeGeom::_type_handle;
 OdeGeom::
 OdeGeom(dGeomID id) :
   _id(id) {
-  odegeom_cat.debug() << get_type() << "(" << _id << ")\n";
+  if (odegeom_cat.is_debug()) {
+    odegeom_cat.debug() << get_type() << "(" << _id << ")\n";
+  }
 }
 
 OdeGeom::
 ~OdeGeom() {
-  odegeom_cat.debug() << "~" << get_type() << "(" << _id << ")\n";
+  if (odegeom_cat.is_debug()) {
+    odegeom_cat.debug() << "~" << get_type() << "(" << _id << ")\n";
+  }
   /*
   GeomSurfaceMap::iterator iter = _geom_surface_map.find(this->get_id());
   if (iter != _geom_surface_map.end()) {

+ 8 - 4
panda/src/ode/odeJoint.cxx

@@ -31,15 +31,19 @@ TypeHandle OdeJoint::_type_handle;
 OdeJoint::
 OdeJoint() :
   _id(nullptr) {
-  std::ostream &out = odejoint_cat.debug();
-  out << get_type() << "(" << _id  << ")\n";
+  if (odejoint_cat.is_debug()) {
+    std::ostream &out = odejoint_cat.debug();
+    out << get_type() << "(" << _id  << ")\n";
+  }
 }
 
 OdeJoint::
 OdeJoint(dJointID id) :
   _id(id) {
-  std::ostream &out = odejoint_cat.debug();
-  out << get_type() << "(" << _id  << ")\n";
+  if (odejoint_cat.is_debug()) {
+    std::ostream &out = odejoint_cat.debug();
+    out << get_type() << "(" << _id  << ")\n";
+  }
 }
 
 OdeJoint::

+ 5 - 3
panda/src/ode/odeSpace.cxx

@@ -157,9 +157,11 @@ auto_callback(void *data, dGeomID o1, dGeomID o2) {
   numc = dCollide(o1, o2, OdeSpace::MAX_CONTACTS, &contact[0].geom, sizeof(dContact));
 
   if (numc) {
-    odespace_cat.debug() << "collision between geoms " << o1 << " and " << o2 << "\n";
-    odespace_cat.debug() << "collision between body " << b1 << " and " << b2 << "\n";
-    odespace_cat.debug() << "surface1= "<< surface1 << " surface2=" << surface2 << "\n";
+    if (odespace_cat.is_debug()) {
+      odespace_cat.debug() << "collision between geoms " << o1 << " and " << o2 << "\n";
+      odespace_cat.debug() << "collision between body " << b1 << " and " << b2 << "\n";
+      odespace_cat.debug() << "surface1= "<< surface1 << " surface2=" << surface2 << "\n";
+    }
 
     PT(OdeCollisionEntry) entry;
     if (!_static_auto_collide_space->_collision_event.empty()) {

+ 3 - 1
panda/src/ode/odeTriMeshData.I

@@ -38,7 +38,9 @@ get(int data_id) {
 
 INLINE void OdeTriMeshData::
 build_single(const void* vertices, int vertex_stride, int vertex_count, const void* indices, int index_count, int tri_stride) {
-  odetrimeshdata_cat.debug() << "build_single(" << vertices << ", " << vertex_stride << ", " << vertex_count << ", " << indices << ", " << index_count << ", " << tri_stride << ")\n";
+  if (odetrimeshdata_cat.is_debug()) {
+    odetrimeshdata_cat.debug() << "build_single(" << vertices << ", " << vertex_stride << ", " << vertex_count << ", " << indices << ", " << index_count << ", " << tri_stride << ")\n";
+  }
   dGeomTriMeshDataBuildSingle(_id, vertices, vertex_stride, vertex_count, indices, index_count, tri_stride);
 }
 

+ 44 - 20
panda/src/ode/odeTriMeshData.cxx

@@ -20,7 +20,9 @@ OdeTriMeshData::TriMeshDataMap *OdeTriMeshData::_tri_mesh_data_map = nullptr;
 
 void OdeTriMeshData::
 link_data(dGeomID id, PT(OdeTriMeshData) data) {
-  odetrimeshdata_cat.debug() << get_class_type() << "::link_data(" << id << "->" << data << ")" << "\n";
+  if (odetrimeshdata_cat.is_debug()) {
+    odetrimeshdata_cat.debug() << get_class_type() << "::link_data(" << id << "->" << data << ")" << "\n";
+  }
   get_tri_mesh_data_map()[id] = data;
 }
 
@@ -36,7 +38,9 @@ get_data(dGeomID id) {
 
 void OdeTriMeshData::
 unlink_data(dGeomID id) {
-  odetrimeshdata_cat.debug() << get_class_type() << "::unlink_data(" << id << ")" << "\n";
+  if (odetrimeshdata_cat.is_debug()) {
+    odetrimeshdata_cat.debug() << get_class_type() << "::unlink_data(" << id << ")" << "\n";
+  }
   nassertv(_tri_mesh_data_map != nullptr);
   TriMeshDataMap::iterator iter = _tri_mesh_data_map->find(id);
   if (iter != _tri_mesh_data_map->end()) {
@@ -46,11 +50,13 @@ unlink_data(dGeomID id) {
 
 void OdeTriMeshData::
 print_data(const std::string &marker) {
-  odetrimeshdata_cat.debug() << get_class_type() << "::print_data(" << marker << ")\n";
-  const TriMeshDataMap &data_map = get_tri_mesh_data_map();
-  TriMeshDataMap::const_iterator iter = data_map.begin();
-  for (;iter != data_map.end(); ++iter) {
-    odetrimeshdata_cat.debug() << "\t" << iter->first << " : " << iter->second << "\n";
+  if (odetrimeshdata_cat.is_debug()) {
+    odetrimeshdata_cat.debug() << get_class_type() << "::print_data(" << marker << ")\n";
+    const TriMeshDataMap &data_map = get_tri_mesh_data_map();
+    TriMeshDataMap::const_iterator iter = data_map.begin();
+    for (;iter != data_map.end(); ++iter) {
+      odetrimeshdata_cat.debug() << "\t" << iter->first << " : " << iter->second << "\n";
+    }
   }
 }
 
@@ -95,11 +101,15 @@ OdeTriMeshData(const NodePath& model, bool use_normals) :
   _normals(nullptr),
   _num_vertices(0),
   _num_faces(0) {
-  odetrimeshdata_cat.debug() << get_type() << "(" << _id << ")" << "\n";
+  if (odetrimeshdata_cat.is_debug()) {
+    odetrimeshdata_cat.debug() << get_type() << "(" << _id << ")" << "\n";
+  }
 
   process_model(model, use_normals);
 
-  write_faces(odetrimeshdata_cat.debug());
+  if (odetrimeshdata_cat.is_debug()) {
+    write_faces(odetrimeshdata_cat.debug());
+  }
 
 #ifdef dSINGLE
   if (!use_normals) {
@@ -131,7 +141,9 @@ OdeTriMeshData(const OdeTriMeshData &other) {
 
 OdeTriMeshData::
 ~OdeTriMeshData() {
-  odetrimeshdata_cat.debug() << "~" << get_type() << "(" << _id << ")" << "\n";
+  if (odetrimeshdata_cat.is_debug()) {
+    odetrimeshdata_cat.debug() << "~" << get_type() << "(" << _id << ")" << "\n";
+  }
   destroy();
   if (_vertices != nullptr) {
     PANDA_FREE_ARRAY(_vertices);
@@ -152,7 +164,9 @@ OdeTriMeshData::
 
 void OdeTriMeshData::
 destroy() {
-  odetrimeshdata_cat.debug() << get_type() << "::destroy(" << _id << ")" << "\n";
+  if (odetrimeshdata_cat.is_debug()) {
+    odetrimeshdata_cat.debug() << get_type() << "::destroy(" << _id << ")" << "\n";
+  }
   if (_id != nullptr) {
     dGeomTriMeshDataDestroy(_id);
     remove_data(this);
@@ -185,8 +199,10 @@ process_model(const NodePath& model, bool &use_normals) {
     analyze((GeomNode*)geomNodePaths[i].node());
   }
 
-  odetrimeshdata_cat.debug() << "Found " << _num_vertices << " vertices.\n";
-  odetrimeshdata_cat.debug() << "Found " << _num_faces << " faces.\n";
+  if (odetrimeshdata_cat.is_debug()) {
+    odetrimeshdata_cat.debug() << "Found " << _num_vertices << " vertices.\n";
+    odetrimeshdata_cat.debug() << "Found " << _num_faces << " faces.\n";
+  }
 
   _vertices = (StridedVertex *)PANDA_MALLOC_ARRAY(_num_vertices * sizeof(StridedVertex));
   _faces = (StridedTri *)PANDA_MALLOC_ARRAY(_num_faces * sizeof(StridedTri));
@@ -195,17 +211,23 @@ process_model(const NodePath& model, bool &use_normals) {
 
   for (int i = 0; i < geomNodePaths.get_num_paths(); ++i) {
     process_geom_node((GeomNode*)geomNodePaths[i].node());
-    odetrimeshdata_cat.debug() << "_num_vertices now at " << _num_vertices << "\n";
+    if (odetrimeshdata_cat.is_debug()) {
+      odetrimeshdata_cat.debug() << "_num_vertices now at " << _num_vertices << "\n";
+    }
   }
 
-  odetrimeshdata_cat.debug() << "Filled " << _num_faces << " triangles(" \
-                      << _num_vertices << " vertices)\n";
+  if (odetrimeshdata_cat.is_debug()) {
+    odetrimeshdata_cat.debug()
+      << "Filled " << _num_faces << " triangles(" << _num_vertices << " vertices)\n";
+  }
 }
 
 void OdeTriMeshData::
 process_geom_node(const GeomNode *geomNode) {
-  ostream &out = odetrimeshdata_cat.debug();
-  out.width(2); out << "" << "process_geom_node(" << *geomNode << ")" << "\n";
+  if (odetrimeshdata_cat.is_debug()) {
+    ostream &out = odetrimeshdata_cat.debug();
+    out.width(2); out << "" << "process_geom_node(" << *geomNode << ")" << "\n";
+  }
   for (int i = 0; i < geomNode->get_num_geoms(); ++i) {
     process_geom(geomNode->get_geom(i));
   }
@@ -213,8 +235,10 @@ process_geom_node(const GeomNode *geomNode) {
 
 void OdeTriMeshData::
 process_geom(const Geom *geom) {
-  ostream &out = odetrimeshdata_cat.debug();
-  out.width(4); out << "" << "process_geom(" << *geom << ")" << "\n";
+  if (odetrimeshdata_cat.is_debug()) {
+    ostream &out = odetrimeshdata_cat.debug();
+    out.width(4); out << "" << "process_geom(" << *geom << ")" << "\n";
+  }
   if (geom->get_primitive_type() != Geom::PT_polygons) {
     return;
   }

+ 9 - 3
panda/src/ode/odeWorld.cxx

@@ -20,7 +20,9 @@ TypeHandle OdeWorld::_type_handle;
 OdeWorld::
 OdeWorld() :
   _id(dWorldCreate()) {
-  odeworld_cat.debug() << get_type() << "(" << _id << ")" << "\n";
+  if (odeworld_cat.is_debug()) {
+    odeworld_cat.debug() << get_type() << "(" << _id << ")" << "\n";
+  }
   _num_surfaces = 0;
 
 }
@@ -34,7 +36,9 @@ OdeWorld(const OdeWorld &copy) :
 
 OdeWorld::
 ~OdeWorld() {
-  odeworld_cat.debug() << "~" << get_type() << "(" << _id << ")" << "\n";
+  if (odeworld_cat.is_debug()) {
+    odeworld_cat.debug() << "~" << get_type() << "(" << _id << ")" << "\n";
+  }
 }
 
 void OdeWorld::
@@ -71,7 +75,9 @@ init_surface_table(uint8_t num_surfaces) {
 
 void OdeWorld::
 set_surface(int pos1, int pos2, sSurfaceParams& entry) {
-  odeworld_cat.debug() << " pos1 " << pos1 << " pos2 " << pos2 << " num surfaces " << (int)_num_surfaces << " endline\n";
+  if (odeworld_cat.is_debug()) {
+    odeworld_cat.debug() << " pos1 " << pos1 << " pos2 " << pos2 << " num surfaces " << (int)_num_surfaces << " endline\n";
+  }
   if((_num_surfaces <= pos1) || (_num_surfaces <= pos2)) {
     odeworld_cat.error() << "surface position exceeds size of surface table, set num_surface in initSurfaceTable higher." << "\n";
     return;

+ 2 - 0
panda/src/pgraph/nodePath.cxx

@@ -3139,6 +3139,8 @@ get_texture(TextureStage *stage) const {
 /**
  * Recursively searches the scene graph for references to the given texture,
  * and replaces them with the new texture.
+ *
+ * @since 1.10.4
  */
 void NodePath::
 replace_texture(Texture *tex, Texture *new_tex) {

+ 2 - 0
panda/src/pgraph/pythonLoaderFileType.h

@@ -24,6 +24,8 @@
  * This defines a Python-based loader plug-in.  An instance of this can be
  * constructed by inheritance and explicitly registered, or it can be created
  * by passing in a pkg_resources.EntryPoint instance.
+ *
+ * @since 1.10.4
  */
 class PythonLoaderFileType : public LoaderFileType {
 public:

+ 3 - 3
panda/src/pgraph/textureAttrib.I

@@ -29,12 +29,12 @@ TextureAttrib() {
 INLINE TextureAttrib::
 TextureAttrib(const TextureAttrib &copy) :
   _on_stages(copy._on_stages),
-  _render_stages(copy._render_stages),
-  _render_ff_stages(copy._render_ff_stages),
+  _render_stages(),
+  _render_ff_stages(),
   _next_implicit_sort(copy._next_implicit_sort),
   _off_stages(copy._off_stages),
   _off_all_stages(copy._off_all_stages),
-  _sort_seq(copy._sort_seq),
+  _sort_seq(UpdateSeq::old()),
   _filtered_seq(UpdateSeq::old())
 {
 }

+ 2 - 13
panda/src/pgraph/textureAttrib.cxx

@@ -116,10 +116,6 @@ add_on_stage(TextureStage *stage, Texture *tex, int override) const {
   (*si)._has_sampler = false;
   ++(attrib->_next_implicit_sort);
 
-  // We now need to re-sort the attrib list.
-  attrib->_sort_seq = UpdateSeq::old();
-  attrib->_filtered_seq = UpdateSeq::old();
-
   return return_new(attrib);
 }
 
@@ -140,10 +136,6 @@ add_on_stage(TextureStage *stage, Texture *tex, const SamplerState &sampler, int
   (*si)._has_sampler = true;
   ++(attrib->_next_implicit_sort);
 
-  // We now need to re-sort the attrib list.
-  attrib->_sort_seq = UpdateSeq::old();
-  attrib->_filtered_seq = UpdateSeq::old();
-
   return return_new(attrib);
 }
 
@@ -158,9 +150,6 @@ remove_on_stage(TextureStage *stage) const {
   Stages::iterator si = attrib->_on_stages.find(StageNode(stage));
   if (si != attrib->_on_stages.end()) {
     attrib->_on_stages.erase(si);
-
-    attrib->_sort_seq = UpdateSeq::old();
-    attrib->_filtered_seq = UpdateSeq::old();
   }
 
   return return_new(attrib);
@@ -182,8 +171,6 @@ add_off_stage(TextureStage *stage, int override) const {
     Stages::iterator si = attrib->_on_stages.find(sn);
     if (si != attrib->_on_stages.end()) {
       attrib->_on_stages.erase(si);
-      attrib->_sort_seq = UpdateSeq::old();
-      attrib->_filtered_seq = UpdateSeq::old();
     }
   }
   return return_new(attrib);
@@ -253,6 +240,8 @@ unify_texture_stages(TextureStage *stage) const {
 /**
  * Returns a new TextureAttrib, just like this one, but with all references to
  * the given texture replaced with the new texture.
+ *
+ * @since 1.10.4
  */
 CPT(RenderAttrib) TextureAttrib::
 replace_texture(Texture *tex, Texture *new_tex) const {

+ 1 - 0
pandatool/src/mayaprogs/mayapath.cxx

@@ -106,6 +106,7 @@ struct MayaVerInfo maya_versions[] = {
   { "MAYA20165", "2016.5"},
   { "MAYA2017", "2017"},
   { "MAYA2018", "2018"},
+  { "MAYA2019", "2019"},
   { 0, 0 },
 };
 

+ 1 - 0
setup.cfg

@@ -20,6 +20,7 @@ classifiers =
     Programming Language :: Python :: 3.5
     Programming Language :: Python :: 3.6
     Programming Language :: Python :: 3.7
+    Programming Language :: Python :: 3.8
     Programming Language :: Python :: Implementation :: CPython
     Topic :: Games/Entertainment
     Topic :: Multimedia

+ 38 - 0
tests/gui/test_DirectEntry.py

@@ -0,0 +1,38 @@
+# coding=utf-8
+from direct.gui.DirectEntry import DirectEntry
+import sys
+
+
+def test_entry_get():
+    entry = DirectEntry()
+    assert isinstance(entry.get(), str)
+
+
+def test_entry_auto_capitalize():
+    # Now we can generate the DirectEntry component itself. In normal use, we
+    # would pass "autoCapitalize=1" to DirectEntry's constructor in order for
+    # DirectEntry._autoCapitalize() to be called upon typing into the entry
+    # GUI, however in the case of this unit test where there is no GUI to type
+    # into, we don't need to bother doing that; we're just calling the function
+    # ourselves anyway.
+    entry = DirectEntry()
+
+    # Test DirectEntry._autoCapitalize(). The intended behavior would be that
+    # the first letter of each word in the entry would be capitalized, so that
+    # is what we will check for:
+    entry.set('auto capitalize test')
+    entry._autoCapitalize()
+    assert entry.get() == 'Auto Capitalize Test'
+
+    # Test DirectEntry._autoCapitalize() with a unicode object this time.
+    entry.set(u'àütò çapítalízè ţèsţ')
+    assert entry.get() == u'àütò çapítalízè ţèsţ'
+    entry._autoCapitalize()
+    assert entry.get() == u'Àütò Çapítalízè Ţèsţ'
+
+    # Also test it with a UTF-8 encoded byte string in Python 2.
+    if sys.version_info < (3, 0):
+        entry.set(u'àütò çapítalízè ţèsţ'.encode('utf-8'))
+        assert entry.get() == u'àütò çapítalízè ţèsţ'
+        entry._autoCapitalize()
+        assert entry.get() == u'Àütò Çapítalízè Ţèsţ'