mirror of
https://github.com/panda3d/panda3d.git
synced 2025-09-30 16:58:40 -04:00
Merge branch 'release/1.10.x' into incoming
This commit is contained in:
commit
665d2fc56b
@ -24,7 +24,7 @@ Installing Panda3D
|
||||
==================
|
||||
|
||||
The latest Panda3D SDK can be downloaded from
|
||||
[this page](https://www.panda3d.org/download/sdk-1-10-4/).
|
||||
[this page](https://www.panda3d.org/download/sdk-1-10-4-1/).
|
||||
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.4/panda3d-1.10.4-tools-win64.zip
|
||||
https://www.panda3d.org/download/panda3d-1.10.4/panda3d-1.10.4-tools-win32.zip
|
||||
https://www.panda3d.org/download/panda3d-1.10.4.1/panda3d-1.10.4.1-tools-win64.zip
|
||||
https://www.panda3d.org/download/panda3d-1.10.4.1/panda3d-1.10.4.1-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.4/panda3d-1.10.4-tools-mac.tar.gz).
|
||||
compile Panda3D, which can be acquired from [here](https://www.panda3d.org/download/panda3d-1.10.4.1/panda3d-1.10.4.1-tools-mac.tar.gz).
|
||||
|
||||
After placing the thirdparty directory inside the panda3d source directory,
|
||||
you may build Panda3D using a command like the following:
|
||||
|
@ -230,7 +230,7 @@ class Loader(DirectObject):
|
||||
|
||||
"""
|
||||
|
||||
assert Loader.notify.debug("Loading model: %s" % (modelPath))
|
||||
assert Loader.notify.debug("Loading model: %s" % (modelPath,))
|
||||
if loaderOptions is None:
|
||||
loaderOptions = LoaderOptions()
|
||||
else:
|
||||
|
@ -1039,7 +1039,7 @@ def MakeInstaller(version, **kwargs):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
version = ParsePandaVersion("dtool/PandaVersion.pp")
|
||||
version = GetMetadataValue('version')
|
||||
|
||||
parser = OptionParser()
|
||||
parser.add_option('', '--version', dest='version', help='Panda3D version number (default: %s)' % (version), default=version)
|
||||
|
@ -15,7 +15,7 @@ import tempfile
|
||||
import subprocess
|
||||
from distutils.sysconfig import get_config_var
|
||||
from optparse import OptionParser
|
||||
from makepandacore import ColorText, LocateBinary, ParsePandaVersion, GetExtensionSuffix, SetVerbose, GetVerbose, GetMetadataValue
|
||||
from makepandacore import ColorText, LocateBinary, GetExtensionSuffix, SetVerbose, GetVerbose, GetMetadataValue
|
||||
from base64 import urlsafe_b64encode
|
||||
|
||||
|
||||
@ -730,7 +730,7 @@ if __debug__:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
version = ParsePandaVersion("dtool/PandaVersion.pp")
|
||||
version = GetMetadataValue('version')
|
||||
|
||||
parser = OptionParser()
|
||||
parser.add_option('', '--version', dest = 'version', help = 'Panda3D version number (default: %s)' % (version), default = version)
|
||||
|
@ -113,6 +113,41 @@ register_deferred_type(const string &extension, const string &library) {
|
||||
_deferred_types[dcextension] = library;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a type previously registered using register_type.
|
||||
*/
|
||||
void LoaderFileTypeRegistry::
|
||||
unregister_type(LoaderFileType *type) {
|
||||
Types::iterator it = find(_types.begin(), _types.end(), type);
|
||||
if (it == _types.end()) {
|
||||
if (loader_cat.is_debug()) {
|
||||
loader_cat.debug()
|
||||
<< "Attempt to unregister LoaderFileType " << type->get_name()
|
||||
<< " (" << type->get_type() << "), which was not registered.\n";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
_types.erase(it);
|
||||
|
||||
{
|
||||
std::string dcextension = downcase(type->get_extension());
|
||||
Extensions::iterator ei = _extensions.find(dcextension);
|
||||
if (ei != _extensions.end() && ei->second == type) {
|
||||
_extensions.erase(ei);
|
||||
}
|
||||
}
|
||||
|
||||
vector_string words;
|
||||
extract_words(type->get_additional_extensions(), words);
|
||||
for (const std::string &word : words) {
|
||||
Extensions::iterator ei = _extensions.find(downcase(word));
|
||||
if (ei != _extensions.end() && ei->second == type) {
|
||||
_extensions.erase(ei);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of types registered.
|
||||
*/
|
||||
|
@ -35,10 +35,14 @@ public:
|
||||
void register_type(LoaderFileType *type);
|
||||
void register_deferred_type(const std::string &extension, const std::string &library);
|
||||
|
||||
void unregister_type(LoaderFileType *type);
|
||||
|
||||
PUBLISHED:
|
||||
EXTENSION(void register_type(PyObject *type));
|
||||
EXTENSION(void register_deferred_type(PyObject *entry_point));
|
||||
|
||||
EXTENSION(void unregister_type(PyObject *type));
|
||||
|
||||
int get_num_types() const;
|
||||
LoaderFileType *get_type(int n) const;
|
||||
MAKE_SEQ(get_types, get_num_types, get_type);
|
||||
|
@ -17,6 +17,8 @@
|
||||
|
||||
#include "pythonLoaderFileType.h"
|
||||
|
||||
extern struct Dtool_PyTypedObject Dtool_LoaderFileType;
|
||||
|
||||
/**
|
||||
* Registers a loader file type that is implemented in Python.
|
||||
*/
|
||||
@ -64,4 +66,50 @@ register_deferred_type(PyObject *entry_point) {
|
||||
_this->register_type(loader);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the given loader type is registered, unregisters it.
|
||||
*/
|
||||
void Extension<LoaderFileTypeRegistry>::
|
||||
unregister_type(PyObject *type) {
|
||||
// Are we passing in a C++ file type object?
|
||||
LoaderFileType *extracted_type;
|
||||
if (DtoolInstance_GetPointer(type, extracted_type, Dtool_LoaderFileType)) {
|
||||
_this->unregister_type(extracted_type);
|
||||
return;
|
||||
}
|
||||
|
||||
// If not, we may be passing in a Python file type.
|
||||
PyObject *load_func = PyObject_GetAttrString(type, "load_file");
|
||||
PyObject *save_func = PyObject_GetAttrString(type, "save_file");
|
||||
PyErr_Clear();
|
||||
|
||||
if (load_func == nullptr && save_func == nullptr) {
|
||||
Dtool_Raise_TypeError("expected loader type");
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep looping until we've removed all instances of it.
|
||||
bool found_any;
|
||||
do {
|
||||
found_any = false;
|
||||
size_t num_types = _this->get_num_types();
|
||||
for (size_t i = 0; i < num_types; ++i) {
|
||||
LoaderFileType *type = _this->get_type(i);
|
||||
if (type->is_of_type(PythonLoaderFileType::get_class_type())) {
|
||||
PythonLoaderFileType *python_type = (PythonLoaderFileType *)type;
|
||||
if (python_type->_load_func == load_func &&
|
||||
python_type->_save_func == save_func) {
|
||||
_this->unregister_type(python_type);
|
||||
delete python_type;
|
||||
found_any = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (found_any);
|
||||
|
||||
Py_XDECREF(load_func);
|
||||
Py_XDECREF(save_func);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
@ -31,6 +31,8 @@ class Extension<LoaderFileTypeRegistry> : public ExtensionBase<LoaderFileTypeReg
|
||||
public:
|
||||
void register_type(PyObject *type);
|
||||
void register_deferred_type(PyObject *entry_point);
|
||||
|
||||
void unregister_type(PyObject *type);
|
||||
};
|
||||
|
||||
#endif // HAVE_PYTHON
|
||||
|
@ -83,10 +83,27 @@ init(PyObject *loader) {
|
||||
// it must occur in the list.
|
||||
PyObject *extensions = PyObject_GetAttrString(loader, "extensions");
|
||||
if (extensions != nullptr) {
|
||||
if (PyUnicode_Check(extensions)
|
||||
#if PY_MAJOR_VERSION < 3
|
||||
|| PyString_Check(extensions)
|
||||
#endif
|
||||
) {
|
||||
Dtool_Raise_TypeError("extensions list should be a list or tuple");
|
||||
Py_DECREF(extensions);
|
||||
return false;
|
||||
}
|
||||
|
||||
PyObject *sequence = PySequence_Fast(extensions, "extensions must be a sequence");
|
||||
PyObject **items = PySequence_Fast_ITEMS(sequence);
|
||||
Py_ssize_t num_items = PySequence_Fast_GET_SIZE(sequence);
|
||||
Py_DECREF(extensions);
|
||||
|
||||
if (num_items == 0) {
|
||||
PyErr_SetString(PyExc_ValueError, "extensions list may not be empty");
|
||||
Py_DECREF(sequence);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool found_extension = false;
|
||||
|
||||
for (Py_ssize_t i = 0; i < num_items; ++i) {
|
||||
|
@ -19,6 +19,7 @@
|
||||
#ifdef HAVE_PYTHON
|
||||
|
||||
#include "loaderFileType.h"
|
||||
#include "extension.h"
|
||||
|
||||
/**
|
||||
* This defines a Python-based loader plug-in. An instance of this can be
|
||||
@ -57,6 +58,8 @@ private:
|
||||
PyObject *_save_func = nullptr;
|
||||
bool _supports_compressed = false;
|
||||
|
||||
friend class Extension<LoaderFileTypeRegistry>;
|
||||
|
||||
public:
|
||||
static TypeHandle get_class_type() {
|
||||
return _type_handle;
|
||||
|
220
tests/pgraph/test_loader_types.py
Normal file
220
tests/pgraph/test_loader_types.py
Normal file
@ -0,0 +1,220 @@
|
||||
from panda3d.core import LoaderFileTypeRegistry, ModelRoot, Loader, LoaderOptions, Filename
|
||||
import pytest
|
||||
import tempfile
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_filename():
|
||||
"""Fixture returning a filename to an existent .test file."""
|
||||
fp = tempfile.NamedTemporaryFile(suffix='.test', delete=False)
|
||||
fp.write(b"test")
|
||||
fp.close()
|
||||
filename = Filename.from_os_specific(fp.name)
|
||||
filename.make_true_case()
|
||||
yield filename
|
||||
os.unlink(fp.name)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_pz_filename():
|
||||
"""Fixture returning a filename to an existent .test.pz file."""
|
||||
fp = tempfile.NamedTemporaryFile(suffix='.test.pz', delete=False)
|
||||
fp.write(b"test")
|
||||
fp.close()
|
||||
filename = Filename.from_os_specific(fp.name)
|
||||
filename.make_true_case()
|
||||
yield filename
|
||||
os.unlink(fp.name)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def registered_type(type):
|
||||
"""Convenience method allowing use of register_type in a with block."""
|
||||
registry = LoaderFileTypeRegistry.get_global_ptr()
|
||||
registry.register_type(type)
|
||||
yield
|
||||
registry.unregister_type(type)
|
||||
|
||||
|
||||
class DummyLoader:
|
||||
"""The simplest possible successful LoaderFileType."""
|
||||
|
||||
extensions = ["test"]
|
||||
|
||||
@staticmethod
|
||||
def load_file(path, options, record=None):
|
||||
return ModelRoot("loaded")
|
||||
|
||||
|
||||
def test_loader_invalid():
|
||||
"""Tests that registering a malformed loader fails."""
|
||||
|
||||
class MissingExtensionsLoader:
|
||||
pass
|
||||
|
||||
class InvalidTypeExtensionsLoader:
|
||||
extensions = "abc"
|
||||
|
||||
class EmptyExtensionsLoader:
|
||||
extensions = []
|
||||
|
||||
class InvalidExtensionsLoader:
|
||||
extensions = [123, None]
|
||||
|
||||
registry = LoaderFileTypeRegistry.get_global_ptr()
|
||||
|
||||
with pytest.raises(Exception):
|
||||
registry.register_type("invalid")
|
||||
|
||||
with pytest.raises(Exception):
|
||||
registry.register_type(MissingExtensionsLoader)
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
registry.register_type(InvalidTypeExtensionsLoader)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
registry.register_type(EmptyExtensionsLoader)
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
registry.register_type(InvalidExtensionsLoader)
|
||||
|
||||
|
||||
def test_loader_success(test_filename):
|
||||
"""Tests that a normal dummy loader successfully loads."""
|
||||
|
||||
with registered_type(DummyLoader):
|
||||
model = Loader.get_global_ptr().load_sync(test_filename, LoaderOptions(LoaderOptions.LF_no_cache))
|
||||
assert model is not None
|
||||
assert model.name == "loaded"
|
||||
|
||||
|
||||
def test_loader_extensions(test_filename):
|
||||
"""Tests multi-extension loaders."""
|
||||
|
||||
class MultiExtensionLoader:
|
||||
extensions = ["test1", "teSt2"]
|
||||
|
||||
@staticmethod
|
||||
def load_file(path, options, record=None):
|
||||
return ModelRoot("loaded")
|
||||
|
||||
fp1 = tempfile.NamedTemporaryFile(suffix='.test1', delete=False)
|
||||
fp1.write(b"test1")
|
||||
fp1.close()
|
||||
fn1 = Filename.from_os_specific(fp1.name)
|
||||
fn1.make_true_case()
|
||||
|
||||
fp2 = tempfile.NamedTemporaryFile(suffix='.TEST2', delete=False)
|
||||
fp2.write(b"test2")
|
||||
fp2.close()
|
||||
fn2 = Filename.from_os_specific(fp2.name)
|
||||
fn2.make_true_case()
|
||||
|
||||
try:
|
||||
with registered_type(MultiExtensionLoader):
|
||||
model1 = Loader.get_global_ptr().load_sync(fn1, LoaderOptions(LoaderOptions.LF_no_cache))
|
||||
assert model1 is not None
|
||||
assert model1.name == "loaded"
|
||||
|
||||
model2 = Loader.get_global_ptr().load_sync(fn2, LoaderOptions(LoaderOptions.LF_no_cache))
|
||||
assert model2 is not None
|
||||
assert model2.name == "loaded"
|
||||
finally:
|
||||
os.unlink(fp1.name)
|
||||
os.unlink(fp2.name)
|
||||
|
||||
# Ensure that both were unregistered.
|
||||
registry = LoaderFileTypeRegistry.get_global_ptr()
|
||||
assert not registry.get_type_from_extension("test1")
|
||||
assert not registry.get_type_from_extension("test2")
|
||||
|
||||
|
||||
def test_loader_nonexistent():
|
||||
"""Verifies that non-existent files fail before calling load_file."""
|
||||
flag = [False]
|
||||
|
||||
class AssertiveLoader:
|
||||
extensions = ["test"]
|
||||
|
||||
@staticmethod
|
||||
def load_file(path, options, record=None):
|
||||
flag[0] = True
|
||||
assert False, "should never get here"
|
||||
|
||||
with registered_type(AssertiveLoader):
|
||||
model = Loader.get_global_ptr().load_sync("/non-existent", LoaderOptions(LoaderOptions.LF_no_cache))
|
||||
assert model is None
|
||||
assert not flag[0]
|
||||
|
||||
|
||||
def test_loader_exception(test_filename):
|
||||
"""Tests for a loader that raises an exception."""
|
||||
|
||||
class FailingLoader:
|
||||
extensions = ["test"]
|
||||
|
||||
@staticmethod
|
||||
def load_file(path, options, record=None):
|
||||
raise Exception("test error")
|
||||
|
||||
with registered_type(FailingLoader):
|
||||
model = Loader.get_global_ptr().load_sync(test_filename, LoaderOptions(LoaderOptions.LF_no_cache))
|
||||
assert model is None
|
||||
|
||||
|
||||
def test_loader_compressed(test_pz_filename):
|
||||
"""Tests for loading .pz files and the supports_compressed flag."""
|
||||
|
||||
class TestLoader:
|
||||
extensions = ["test"]
|
||||
|
||||
@staticmethod
|
||||
def load_file(path, options, record=None):
|
||||
return ModelRoot("loaded")
|
||||
|
||||
# Test with property absent
|
||||
with registered_type(TestLoader):
|
||||
model = Loader.get_global_ptr().load_sync(test_pz_filename, LoaderOptions(LoaderOptions.LF_no_cache))
|
||||
assert model is None
|
||||
|
||||
# Test with property False, should give same result
|
||||
TestLoader.supports_compressed = False
|
||||
with registered_type(TestLoader):
|
||||
model = Loader.get_global_ptr().load_sync(test_pz_filename, LoaderOptions(LoaderOptions.LF_no_cache))
|
||||
assert model is None
|
||||
|
||||
# Test with property True, should work
|
||||
TestLoader.supports_compressed = True
|
||||
with registered_type(TestLoader):
|
||||
model = Loader.get_global_ptr().load_sync(test_pz_filename, LoaderOptions(LoaderOptions.LF_no_cache))
|
||||
assert model is not None
|
||||
assert model.name == "loaded"
|
||||
|
||||
# Test with property invalid type, should not register
|
||||
TestLoader.supports_compressed = None
|
||||
with pytest.raises(TypeError):
|
||||
LoaderFileTypeRegistry.get_global_ptr().register_type(TestLoader)
|
||||
|
||||
|
||||
def test_loader_ram_cache(test_filename):
|
||||
"""Tests that the Python loader plug-ins write to the RAM cache."""
|
||||
|
||||
# Ensure a clean slate.
|
||||
from panda3d.core import ModelPool
|
||||
ModelPool.release_all_models()
|
||||
|
||||
with registered_type(DummyLoader):
|
||||
model1 = Loader.get_global_ptr().load_sync(test_filename, LoaderOptions(LoaderOptions.LF_no_disk_cache | LoaderOptions.LF_allow_instance))
|
||||
assert model1 is not None
|
||||
assert model1.name == "loaded"
|
||||
|
||||
assert ModelPool.has_model(test_filename)
|
||||
assert ModelPool.get_model(test_filename, True) == model1
|
||||
|
||||
model2 = Loader.get_global_ptr().load_sync(test_filename, LoaderOptions(LoaderOptions.LF_cache_only | LoaderOptions.LF_allow_instance))
|
||||
assert model2 is not None
|
||||
assert model1 == model2
|
||||
|
||||
ModelPool.release_model(model2)
|
70
tests/showbase/test_Loader.py
Normal file
70
tests/showbase/test_Loader.py
Normal file
@ -0,0 +1,70 @@
|
||||
from panda3d.core import Filename, NodePath
|
||||
from direct.showbase.Loader import Loader
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def loader():
|
||||
return Loader(base=None)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_model():
|
||||
from panda3d.core import ModelPool, ModelRoot
|
||||
|
||||
root = ModelRoot('model')
|
||||
root.fullpath = '/test-model.bam'
|
||||
|
||||
ModelPool.add_model(root.fullpath, root)
|
||||
yield root.fullpath
|
||||
ModelPool.release_model(root.fullpath)
|
||||
|
||||
|
||||
def test_load_model_filename(loader, temp_model):
|
||||
model = loader.load_model(Filename(temp_model))
|
||||
assert model
|
||||
assert isinstance(model, NodePath)
|
||||
assert model.name == 'model'
|
||||
|
||||
|
||||
def test_load_model_str(loader, temp_model):
|
||||
model = loader.load_model(str(temp_model))
|
||||
assert model
|
||||
assert isinstance(model, NodePath)
|
||||
assert model.name == 'model'
|
||||
|
||||
|
||||
def test_load_model_list(loader, temp_model):
|
||||
models = loader.load_model([temp_model, temp_model])
|
||||
assert models
|
||||
assert isinstance(models, list)
|
||||
assert len(models) == 2
|
||||
assert isinstance(models[0], NodePath)
|
||||
assert isinstance(models[1], NodePath)
|
||||
|
||||
|
||||
def test_load_model_tuple(loader, temp_model):
|
||||
models = loader.load_model((temp_model, temp_model))
|
||||
assert models
|
||||
assert isinstance(models, list)
|
||||
assert len(models) == 2
|
||||
assert isinstance(models[0], NodePath)
|
||||
assert isinstance(models[1], NodePath)
|
||||
|
||||
|
||||
def test_load_model_set(loader, temp_model):
|
||||
models = loader.load_model({temp_model})
|
||||
assert models
|
||||
assert isinstance(models, list)
|
||||
assert len(models) == 1
|
||||
assert isinstance(models[0], NodePath)
|
||||
|
||||
|
||||
def test_load_model_missing(loader):
|
||||
with pytest.raises(IOError):
|
||||
loader.load_model('/nonexistent.bam')
|
||||
|
||||
|
||||
def test_load_model_okmissing(loader):
|
||||
model = loader.load_model('/nonexistent.bam', okMissing=True)
|
||||
assert model is None
|
Loading…
x
Reference in New Issue
Block a user