From 1ca0e3f1eaffd3676481be9f2efeb2b3220d31f8 Mon Sep 17 00:00:00 2001 From: WMOkiishi Date: Mon, 9 Oct 2023 01:46:32 -0600 Subject: [PATCH 01/29] directnotify: annotate types (#1527) --- direct/src/directnotify/DirectNotify.py | 62 +++++++------ direct/src/directnotify/Logger.py | 24 ++--- direct/src/directnotify/Notifier.py | 83 +++++++++--------- direct/src/directnotify/RotatingLog.py | 48 ++++++---- .../distributed/DistributedCartesianGrid.py | 2 +- tests/directnotify/test_DirectNotify.py | 57 ++++++++++++ tests/directnotify/test_Logger.py | 21 +++++ tests/directnotify/test_Notifier.py | 87 +++++++++++++++++++ tests/directnotify/test_RotatingLog.py | 45 ++++++++++ 9 files changed, 333 insertions(+), 96 deletions(-) create mode 100644 tests/directnotify/test_DirectNotify.py create mode 100644 tests/directnotify/test_Logger.py create mode 100644 tests/directnotify/test_Notifier.py create mode 100644 tests/directnotify/test_RotatingLog.py diff --git a/direct/src/directnotify/DirectNotify.py b/direct/src/directnotify/DirectNotify.py index d2e2ba4a3e..f7c7e6c964 100644 --- a/direct/src/directnotify/DirectNotify.py +++ b/direct/src/directnotify/DirectNotify.py @@ -2,6 +2,10 @@ DirectNotify module: this module contains the DirectNotify class """ +from __future__ import annotations + +from panda3d.core import StreamWriter + from . import Notifier from . import Logger @@ -12,39 +16,39 @@ class DirectNotify: mulitple notify categories via a dictionary of Notifiers. """ - def __init__(self): + def __init__(self) -> None: """ DirectNotify class keeps a dictionary of Notfiers """ - self.__categories = {} + self.__categories: dict[str, Notifier.Notifier] = {} # create a default log file self.logger = Logger.Logger() # This will get filled in later by ShowBase.py with a # C++-level StreamWriter object for writing to standard # output. - self.streamWriter = None + self.streamWriter: StreamWriter | None = None - def __str__(self): + def __str__(self) -> str: """ Print handling routine """ return "DirectNotify categories: %s" % (self.__categories) #getters and setters - def getCategories(self): + def getCategories(self) -> list[str]: """ Return list of category dictionary keys """ return list(self.__categories.keys()) - def getCategory(self, categoryName): + def getCategory(self, categoryName: str) -> Notifier.Notifier | None: """getCategory(self, string) Return the category with given name if present, None otherwise """ return self.__categories.get(categoryName, None) - def newCategory(self, categoryName, logger=None): + def newCategory(self, categoryName: str, logger: Logger.Logger | None = None) -> Notifier.Notifier: """newCategory(self, string) Make a new notify category named categoryName. Return new category if no such category exists, else return existing category @@ -52,9 +56,11 @@ class DirectNotify: if categoryName not in self.__categories: self.__categories[categoryName] = Notifier.Notifier(categoryName, logger) self.setDconfigLevel(categoryName) - return self.getCategory(categoryName) + notifier = self.getCategory(categoryName) + assert notifier is not None + return notifier - def setDconfigLevel(self, categoryName): + def setDconfigLevel(self, categoryName: str) -> None: """ Check to see if this category has a dconfig variable to set the notify severity and then set that level. You cannot @@ -77,40 +83,42 @@ class DirectNotify: level = 'error' category = self.getCategory(categoryName) + assert category is not None, f'failed to find category: {categoryName!r}' # Note - this print statement is making it difficult to # achieve "no output unless there's an error" operation - Josh # print ("Setting DirectNotify category: " + categoryName + # " to severity: " + level) if level == "error": - category.setWarning(0) - category.setInfo(0) - category.setDebug(0) + category.setWarning(False) + category.setInfo(False) + category.setDebug(False) elif level == "warning": - category.setWarning(1) - category.setInfo(0) - category.setDebug(0) + category.setWarning(True) + category.setInfo(False) + category.setDebug(False) elif level == "info": - category.setWarning(1) - category.setInfo(1) - category.setDebug(0) + category.setWarning(True) + category.setInfo(True) + category.setDebug(False) elif level == "debug": - category.setWarning(1) - category.setInfo(1) - category.setDebug(1) + category.setWarning(True) + category.setInfo(True) + category.setDebug(True) else: print("DirectNotify: unknown notify level: " + str(level) + " for category: " + str(categoryName)) - def setDconfigLevels(self): + def setDconfigLevels(self) -> None: for categoryName in self.getCategories(): self.setDconfigLevel(categoryName) - def setVerbose(self): + def setVerbose(self) -> None: for categoryName in self.getCategories(): category = self.getCategory(categoryName) - category.setWarning(1) - category.setInfo(1) - category.setDebug(1) + assert category is not None + category.setWarning(True) + category.setInfo(True) + category.setDebug(True) def popupControls(self, tl = None): # Don't use a regular import, to prevent ModuleFinder from picking @@ -119,5 +127,5 @@ class DirectNotify: NotifyPanel = importlib.import_module('direct.tkpanels.NotifyPanel') NotifyPanel.NotifyPanel(self, tl) - def giveNotify(self,cls): + def giveNotify(self, cls) -> None: cls.notify = self.newCategory(cls.__name__) diff --git a/direct/src/directnotify/Logger.py b/direct/src/directnotify/Logger.py index 0c5aeaab04..21418592ec 100644 --- a/direct/src/directnotify/Logger.py +++ b/direct/src/directnotify/Logger.py @@ -1,27 +1,30 @@ """Logger module: contains the logger class which creates and writes data to log files on disk""" +from __future__ import annotations + +import io import time import math class Logger: - def __init__(self, fileName="log"): + def __init__(self, fileName: str = "log") -> None: """ Logger constructor """ - self.__timeStamp = 1 + self.__timeStamp = True self.__startTime = 0.0 - self.__logFile = None + self.__logFile: io.TextIOWrapper | None = None self.__logFileName = fileName - def setTimeStamp(self, enable): + def setTimeStamp(self, enable: bool) -> None: """ Toggle time stamp printing with log entries on and off """ self.__timeStamp = enable - def getTimeStamp(self): + def getTimeStamp(self) -> bool: """ Return whether or not we are printing time stamps with log entries """ @@ -29,24 +32,25 @@ class Logger: # logging control - def resetStartTime(self): + def resetStartTime(self) -> None: """ Reset the start time of the log file for time stamps """ self.__startTime = time.time() - def log(self, entryString): + def log(self, entryString: str) -> None: """log(self, string) Print the given string to the log file""" if self.__logFile is None: self.__openLogFile() + assert self.__logFile is not None if self.__timeStamp: self.__logFile.write(self.__getTimeStamp()) self.__logFile.write(entryString + '\n') # logging functions - def __openLogFile(self): + def __openLogFile(self) -> None: """ Open a file for logging error/warning messages """ @@ -56,14 +60,14 @@ class Logger: logFileName = self.__logFileName + "." + st self.__logFile = open(logFileName, "w") - def __closeLogFile(self): + def __closeLogFile(self) -> None: """ Close the error/warning output file """ if self.__logFile is not None: self.__logFile.close() - def __getTimeStamp(self): + def __getTimeStamp(self) -> str: """ Return the offset between current time and log file startTime """ diff --git a/direct/src/directnotify/Notifier.py b/direct/src/directnotify/Notifier.py index 8a49ab5564..bf3f14779a 100644 --- a/direct/src/directnotify/Notifier.py +++ b/direct/src/directnotify/Notifier.py @@ -2,11 +2,16 @@ Notifier module: contains methods for handling information output for the programmer/user """ + +from __future__ import annotations + +from .Logger import Logger from .LoggerGlobal import defaultLogger from direct.showbase import PythonUtil from panda3d.core import ConfigVariableBool, NotifyCategory, StreamWriter, Notify import time import sys +from typing import NoReturn class NotifierException(Exception): @@ -20,13 +25,13 @@ class Notifier: # messages instead of writing them to the console. This is # particularly useful for integrating the Python notify system # with the C++ notify system. - streamWriter = None + streamWriter: StreamWriter | None = None if ConfigVariableBool('notify-integrate', True): streamWriter = StreamWriter(Notify.out(), False) showTime = ConfigVariableBool('notify-timestamp', False) - def __init__(self, name, logger=None): + def __init__(self, name: str, logger: Logger | None = None) -> None: """ Parameters: name (str): a string name given to this Notifier instance. @@ -42,12 +47,12 @@ class Notifier: self.__logger = logger # Global default levels are initialized here - self.__info = 1 - self.__warning = 1 - self.__debug = 0 - self.__logging = 0 + self.__info = True + self.__warning = True + self.__debug = False + self.__logging = False - def setServerDelta(self, delta, timezone): + def setServerDelta(self, delta: float, timezone: int) -> None: """ Call this method on any Notify object to globally change the timestamp printed for each line of all Notify objects. @@ -65,7 +70,7 @@ class Notifier: self.info("Notify clock adjusted by %s (and timezone adjusted by %s hours) to synchronize with server." % (PythonUtil.formatElapsedSeconds(delta), (time.timezone - timezone) / 3600)) - def getTime(self): + def getTime(self) -> str: """ Return the time as a string suitable for printing at the head of any notify message @@ -74,14 +79,14 @@ class Notifier: # the task is out of focus on win32. time.clock doesn't have this problem. return time.strftime(":%m-%d-%Y %H:%M:%S ", time.localtime(time.time() + self.serverDelta)) - def getOnlyTime(self): + def getOnlyTime(self) -> str: """ Return the time as a string. The Only in the name is referring to not showing the date. """ return time.strftime("%H:%M:%S", time.localtime(time.time() + self.serverDelta)) - def __str__(self): + def __str__(self) -> str: """ Print handling routine """ @@ -89,26 +94,26 @@ class Notifier: (self.__name, self.__info, self.__warning, self.__debug, self.__logging) # Severity funcs - def setSeverity(self, severity): + def setSeverity(self, severity: int) -> None: from panda3d.core import NSDebug, NSInfo, NSWarning, NSError if severity >= NSError: - self.setWarning(0) - self.setInfo(0) - self.setDebug(0) + self.setWarning(False) + self.setInfo(False) + self.setDebug(False) elif severity == NSWarning: - self.setWarning(1) - self.setInfo(0) - self.setDebug(0) + self.setWarning(True) + self.setInfo(False) + self.setDebug(False) elif severity == NSInfo: - self.setWarning(1) - self.setInfo(1) - self.setDebug(0) + self.setWarning(True) + self.setInfo(True) + self.setDebug(False) elif severity <= NSDebug: - self.setWarning(1) - self.setInfo(1) - self.setDebug(1) + self.setWarning(True) + self.setInfo(True) + self.setDebug(True) - def getSeverity(self): + def getSeverity(self) -> int: from panda3d.core import NSDebug, NSInfo, NSWarning, NSError if self.getDebug(): return NSDebug @@ -120,7 +125,7 @@ class Notifier: return NSError # error funcs - def error(self, errorString, exception=NotifierException): + def error(self, errorString: object, exception: type[Exception] = NotifierException) -> NoReturn: """ Raise an exception with given string and optional type: Exception: error @@ -134,7 +139,7 @@ class Notifier: raise exception(errorString) # warning funcs - def warning(self, warningString): + def warning(self, warningString: object) -> int: """ Issue the warning message if warn flag is on """ @@ -148,20 +153,20 @@ class Notifier: self.__print(string) return 1 # to allow assert myNotify.warning("blah") - def setWarning(self, enable): + def setWarning(self, enable: bool) -> None: """ Enable/Disable the printing of warning messages """ self.__warning = enable - def getWarning(self): + def getWarning(self) -> bool: """ Return whether the printing of warning messages is on or off """ return self.__warning # debug funcs - def debug(self, debugString): + def debug(self, debugString: object) -> int: """ Issue the debug message if debug flag is on """ @@ -175,20 +180,20 @@ class Notifier: self.__print(string) return 1 # to allow assert myNotify.debug("blah") - def setDebug(self, enable): + def setDebug(self, enable: bool) -> None: """ Enable/Disable the printing of debug messages """ self.__debug = enable - def getDebug(self): + def getDebug(self) -> bool: """ Return whether the printing of debug messages is on or off """ return self.__debug # info funcs - def info(self, infoString): + def info(self, infoString: object) -> int: """ Print the given informational string, if info flag is on """ @@ -202,39 +207,39 @@ class Notifier: self.__print(string) return 1 # to allow assert myNotify.info("blah") - def getInfo(self): + def getInfo(self) -> bool: """ Return whether the printing of info messages is on or off """ return self.__info - def setInfo(self, enable): + def setInfo(self, enable: bool) -> None: """ Enable/Disable informational message printing """ self.__info = enable # log funcs - def __log(self, logEntry): + def __log(self, logEntry: str) -> None: """ Determine whether to send informational message to the logger """ if self.__logging: self.__logger.log(logEntry) - def getLogging(self): + def getLogging(self) -> bool: """ Return 1 if logging enabled, 0 otherwise """ return self.__logging - def setLogging(self, enable): + def setLogging(self, enable: bool) -> None: """ Set the logging flag to int (1=on, 0=off) """ self.__logging = enable - def __print(self, string): + def __print(self, string: str) -> None: """ Prints the string to output followed by a newline. """ @@ -285,7 +290,7 @@ class Notifier: self.__print(string) return 1 # to allow assert self.notify.debugStateCall(self) - def debugCall(self, debugString=''): + def debugCall(self, debugString: object = '') -> int: """ If this notify is in debug mode, print the time of the call followed by the notifier category and diff --git a/direct/src/directnotify/RotatingLog.py b/direct/src/directnotify/RotatingLog.py index 1502ad8994..f73ab4315b 100755 --- a/direct/src/directnotify/RotatingLog.py +++ b/direct/src/directnotify/RotatingLog.py @@ -1,5 +1,8 @@ +from __future__ import annotations + import os import time +from typing import Iterable class RotatingLog: @@ -8,7 +11,12 @@ class RotatingLog: to a new file if the prior file is too large or after a time interval. """ - def __init__(self, path="./log_file", hourInterval=24, megabyteLimit=1024): + def __init__( + self, + path: str = "./log_file", + hourInterval: int | None = 24, + megabyteLimit: int | None = 1024, + ) -> None: """ Args: path: a full or partial path with file name. @@ -28,33 +36,33 @@ class RotatingLog: if megabyteLimit is not None: self.sizeLimit = megabyteLimit*1024*1024 - def __del__(self): + def __del__(self) -> None: self.close() - def close(self): + def close(self) -> None: if hasattr(self, "file"): self.file.flush() self.file.close() self.closed = self.file.closed del self.file else: - self.closed = 1 + self.closed = True - def shouldRotate(self): + def shouldRotate(self) -> bool: """ Returns a bool about whether a new log file should be created and written to (while at the same time stopping output to the old log file and closing it). """ if not hasattr(self, "file"): - return 1 + return True if self.timeLimit is not None and time.time() > self.timeLimit: - return 1 + return True if self.sizeLimit is not None and self.file.tell() > self.sizeLimit: - return 1 - return 0 + return True + return False - def filePath(self): + def filePath(self) -> str: dateString = time.strftime("%Y_%m_%d_%H", time.localtime()) for i in range(26): limit = self.sizeLimit @@ -65,7 +73,7 @@ class RotatingLog: # Maybe we should clear the self.sizeLimit here... maybe. return path - def rotate(self): + def rotate(self) -> None: """ Rotate the log now. You normally shouldn't need to call this. See write(). @@ -88,12 +96,13 @@ class RotatingLog: #self.newlines = self.file.newlines # Python 2.3, maybe if self.timeLimit is not None and time.time() > self.timeLimit: + assert self.timeInterval is not None self.timeLimit=time.time()+self.timeInterval else: # We'll keep writing to the old file, if available. print("RotatingLog error: Unable to open new log file \"%s\"." % (path,)) - def write(self, data): + def write(self, data: str) -> int | None: """ Write the data to either the current log or a new one, depending on the return of shouldRotate() and whether @@ -105,14 +114,15 @@ class RotatingLog: r = self.file.write(data) self.file.flush() return r + return None - def flush(self): + def flush(self) -> None: return self.file.flush() - def fileno(self): + def fileno(self) -> int: return self.file.fileno() - def isatty(self): + def isatty(self) -> bool: return self.file.isatty() def __next__(self): @@ -131,14 +141,14 @@ class RotatingLog: def xreadlines(self): return self.file.xreadlines() - def seek(self, offset, whence=0): + def seek(self, offset: int, whence: int = 0) -> int: return self.file.seek(offset, whence) - def tell(self): + def tell(self) -> int: return self.file.tell() - def truncate(self, size): + def truncate(self, size: int | None) -> int: return self.file.truncate(size) - def writelines(self, sequence): + def writelines(self, sequence: Iterable[str]) -> None: return self.file.writelines(sequence) diff --git a/direct/src/distributed/DistributedCartesianGrid.py b/direct/src/distributed/DistributedCartesianGrid.py index 88c0af7ce9..39e8b212dd 100755 --- a/direct/src/distributed/DistributedCartesianGrid.py +++ b/direct/src/distributed/DistributedCartesianGrid.py @@ -23,7 +23,7 @@ GRID_Z_OFFSET = 0.0 class DistributedCartesianGrid(DistributedNode, CartesianGridBase): notify = directNotify.newCategory("DistributedCartesianGrid") - notify.setDebug(0) + notify.setDebug(False) VisualizeGrid = ConfigVariableBool("visualize-cartesian-grid", False) diff --git a/tests/directnotify/test_DirectNotify.py b/tests/directnotify/test_DirectNotify.py new file mode 100644 index 0000000000..c98aeb2646 --- /dev/null +++ b/tests/directnotify/test_DirectNotify.py @@ -0,0 +1,57 @@ +import pytest +from panda3d import core +from direct.directnotify import DirectNotify, Logger, Notifier + +CATEGORY_NAME = 'test' + + +@pytest.fixture +def notify(): + notify = DirectNotify.DirectNotify() + notify.newCategory(CATEGORY_NAME) + return notify + + +def test_categories(): + notify = DirectNotify.DirectNotify() + assert len(notify.getCategories()) == 0 + assert notify.getCategory(CATEGORY_NAME) is None + notifier = notify.newCategory(CATEGORY_NAME, logger=Logger.Logger()) + assert isinstance(notifier, Notifier.Notifier) + assert notify.getCategories() == [CATEGORY_NAME] + + +def test_setDconfigLevels(notify): + config = core.ConfigVariableString('notify-level-' + CATEGORY_NAME, '') + notifier = notify.getCategory(CATEGORY_NAME) + config.value = 'error' + notify.setDconfigLevels() + assert notifier.getSeverity() == core.NS_error + config.value = 'warning' + notify.setDconfigLevels() + assert notifier.getSeverity() == core.NS_warning + config.value = 'info' + notify.setDconfigLevels() + assert notifier.getSeverity() == core.NS_info + config.value = 'debug' + notify.setDconfigLevels() + assert notifier.getSeverity() == core.NS_debug + + +def test_setVerbose(notify): + notifier = notify.getCategory(CATEGORY_NAME) + notifier.setWarning(False) + notifier.setInfo(False) + notifier.setDebug(False) + notify.setVerbose() + assert notifier.getWarning() + assert notifier.getInfo() + assert notifier.getDebug() + + +def test_giveNotify(notify): + class HasNotify: + notify = None + + notify.giveNotify(HasNotify) + assert isinstance(HasNotify.notify, Notifier.Notifier) diff --git a/tests/directnotify/test_Logger.py b/tests/directnotify/test_Logger.py new file mode 100644 index 0000000000..1f0aa76f94 --- /dev/null +++ b/tests/directnotify/test_Logger.py @@ -0,0 +1,21 @@ +import re +from direct.directnotify import Logger + +LOG_TEXT = 'Arbitrary log text' + + +def test_logging(tmp_path): + log_filename = str(tmp_path / 'log') + logger = Logger.Logger(log_filename) + + assert logger.getTimeStamp() + logger.log(LOG_TEXT) + logger.setTimeStamp(False) + assert not logger.getTimeStamp() + logger.log(LOG_TEXT) + logger._Logger__closeLogFile() + + log_file, = tmp_path.iterdir() + log_text = log_file.read_text() + pattern = rf'\d\d:\d\d:\d\d:\d\d: {LOG_TEXT}\n{LOG_TEXT}\n' + assert re.match(pattern, log_text) diff --git a/tests/directnotify/test_Notifier.py b/tests/directnotify/test_Notifier.py new file mode 100644 index 0000000000..66acc4c5ce --- /dev/null +++ b/tests/directnotify/test_Notifier.py @@ -0,0 +1,87 @@ +import io +import re +import time +import pytest +from panda3d import core +from direct.directnotify import Logger, Notifier + +NOTIFIER_NAME = 'Test notifier' +DEBUG_LOG = 'Debug log' +INFO_LOG = 'Info log' +WARNING_LOG = 'Warning log' +ERROR_LOG = 'Error log' + + +@pytest.fixture +def log_io(): + return io.StringIO() + + +@pytest.fixture +def notifier(log_io): + logger = Logger.Logger() + logger.setTimeStamp(False) + logger._Logger__logFile = log_io + notifier = Notifier.Notifier(NOTIFIER_NAME, logger) + notifier.setLogging(True) + return notifier + + +def test_setServerDelta(): + notifier = Notifier.Notifier(NOTIFIER_NAME) + notifier.setServerDelta(4.2, time.timezone) + assert Notifier.Notifier.serverDelta == 4 + Notifier.Notifier.serverDelta = 0 + + +def test_logging(notifier, log_io): + notifier.setLogging(False) + assert not notifier.getLogging() + notifier.info(INFO_LOG) + assert log_io.getvalue() == '' + + notifier.setLogging(True) + assert notifier.getLogging() + notifier.info(INFO_LOG) + assert log_io.getvalue() == f':{NOTIFIER_NAME}: {INFO_LOG}\n' + + +@pytest.mark.parametrize('severity', (core.NS_debug, core.NS_info, core.NS_warning, core.NS_error)) +def test_severity(severity, notifier, log_io): + notifier.setSeverity(severity) + assert notifier.getSeverity() == severity + + with pytest.raises(Notifier.NotifierException): + notifier.error(ERROR_LOG) + warning_return = notifier.warning(WARNING_LOG) + info_return = notifier.info(INFO_LOG) + debug_return = notifier.debug(DEBUG_LOG) + assert warning_return and info_return and debug_return + + expected_logs = [ + f'{Notifier.NotifierException}: {NOTIFIER_NAME}(error): {ERROR_LOG}', + f':{NOTIFIER_NAME}(warning): {WARNING_LOG}', + f':{NOTIFIER_NAME}: {INFO_LOG}', + f':{NOTIFIER_NAME}(debug): {DEBUG_LOG}', + ] + del expected_logs[6-severity:] + assert log_io.getvalue() == '\n'.join(expected_logs) + '\n' + + +def test_custom_exception(notifier): + class CustomException(Exception): + pass + + with pytest.raises(CustomException): + notifier.error(ERROR_LOG, CustomException) + + +def test_debugCall(notifier, log_io): + notifier.setDebug(False) + return_value = notifier.debugCall(DEBUG_LOG) + assert return_value + assert log_io.getvalue() == '' + notifier.setDebug(True) + notifier.debugCall(DEBUG_LOG) + pattern = rf':\d\d:\d\d:\d\d:{NOTIFIER_NAME} "{DEBUG_LOG}" test_debugCall\(.*\)\n' + assert re.match(pattern, log_io.getvalue()) diff --git a/tests/directnotify/test_RotatingLog.py b/tests/directnotify/test_RotatingLog.py new file mode 100644 index 0000000000..5ca66503c5 --- /dev/null +++ b/tests/directnotify/test_RotatingLog.py @@ -0,0 +1,45 @@ +import pytest +from direct.directnotify import RotatingLog + +LOG_TEXT = 'Arbitrary log text' + + +@pytest.fixture +def log_dir(tmp_path): + log_dir = tmp_path / 'logs' + log_dir.mkdir() + return log_dir + + +@pytest.fixture +def rotating_log(log_dir): + log_filename = str(log_dir / 'log') + rotating_log = RotatingLog.RotatingLog(log_filename) + yield rotating_log + rotating_log.close() + + +def test_rotation(rotating_log, log_dir): + rotating_log.sizeLimit = -1 + rotating_log.write('1') + rotating_log.write('2') + written = [f.read_text() for f in log_dir.iterdir()] + assert written == ['1', '2'] or written == ['2', '1'] + + +def test_wrapper_methods(rotating_log, log_dir): + rotating_log.write('') + log_file, = log_dir.iterdir() + + assert rotating_log.fileno() == rotating_log.file.fileno() + assert rotating_log.isatty() == rotating_log.file.isatty() + + rotating_log.writelines([LOG_TEXT] * 10) + assert not log_file.read_text() + rotating_log.flush() + assert log_file.read_text() == LOG_TEXT * 10 + + assert rotating_log.tell() == len(LOG_TEXT) * 10 + rotating_log.seek(len(LOG_TEXT)) + rotating_log.truncate(None) + assert log_file.read_text() == LOG_TEXT From 86aa437804888d8a126875437b4f1cc4431d4e07 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 9 Oct 2023 12:49:04 +0200 Subject: [PATCH 02/29] dtoolbase: Fix static init ordering regression This was a regression in bf65624298b9a5ea49cb637fadb4fc6f7c85ce9b that caused crashes on startup in static builds due to the "small" DeletedBufferChain array not being initialized early enough For some reason it wasn't being constant-initialized, it is now by setting the _buffer_size field to 0 initially and changing it later in get_deleted_chain --- dtool/src/dtoolbase/deletedBufferChain.I | 18 +-------- dtool/src/dtoolbase/deletedBufferChain.cxx | 45 +++++++++------------- dtool/src/dtoolbase/deletedBufferChain.h | 16 ++------ 3 files changed, 23 insertions(+), 56 deletions(-) diff --git a/dtool/src/dtoolbase/deletedBufferChain.I b/dtool/src/dtoolbase/deletedBufferChain.I index f74c9dd5eb..2b79283ea8 100644 --- a/dtool/src/dtoolbase/deletedBufferChain.I +++ b/dtool/src/dtoolbase/deletedBufferChain.I @@ -16,7 +16,7 @@ * size. */ constexpr DeletedBufferChain:: -DeletedBufferChain(size_t buffer_size) : _buffer_size(buffer_size) { +DeletedBufferChain(size_t buffer_size) noexcept : _buffer_size(buffer_size) { } /** @@ -77,22 +77,6 @@ operator < (const DeletedBufferChain &other) const { return _buffer_size < other._buffer_size; } -/** - * Returns a deleted chain of the given size. - */ -INLINE DeletedBufferChain *DeletedBufferChain:: -get_deleted_chain(size_t buffer_size) { - // We must allocate at least this much space for bookkeeping reasons. - buffer_size = (std::max)(buffer_size, sizeof(ObjectNode)); - - size_t index = ((buffer_size + sizeof(void *) - 1) / sizeof(void *)) - 1; - if (index < num_small_deleted_chains) { - return &_small_deleted_chains[index]; - } else { - return get_large_deleted_chain((index + 1) * sizeof(void *)); - } -} - /** * Casts an ObjectNode* to a void* buffer. */ diff --git a/dtool/src/dtoolbase/deletedBufferChain.cxx b/dtool/src/dtoolbase/deletedBufferChain.cxx index de74173175..9821129254 100644 --- a/dtool/src/dtoolbase/deletedBufferChain.cxx +++ b/dtool/src/dtoolbase/deletedBufferChain.cxx @@ -16,32 +16,10 @@ #include -DeletedBufferChain DeletedBufferChain::_small_deleted_chains[DeletedBufferChain::num_small_deleted_chains] = { - DeletedBufferChain(sizeof(void *)), - DeletedBufferChain(sizeof(void *) * 2), - DeletedBufferChain(sizeof(void *) * 3), - DeletedBufferChain(sizeof(void *) * 4), - DeletedBufferChain(sizeof(void *) * 5), - DeletedBufferChain(sizeof(void *) * 6), - DeletedBufferChain(sizeof(void *) * 7), - DeletedBufferChain(sizeof(void *) * 8), - DeletedBufferChain(sizeof(void *) * 9), - DeletedBufferChain(sizeof(void *) * 10), - DeletedBufferChain(sizeof(void *) * 11), - DeletedBufferChain(sizeof(void *) * 12), - DeletedBufferChain(sizeof(void *) * 13), - DeletedBufferChain(sizeof(void *) * 14), - DeletedBufferChain(sizeof(void *) * 15), - DeletedBufferChain(sizeof(void *) * 16), - DeletedBufferChain(sizeof(void *) * 17), - DeletedBufferChain(sizeof(void *) * 18), - DeletedBufferChain(sizeof(void *) * 19), - DeletedBufferChain(sizeof(void *) * 20), - DeletedBufferChain(sizeof(void *) * 21), - DeletedBufferChain(sizeof(void *) * 22), - DeletedBufferChain(sizeof(void *) * 23), - DeletedBufferChain(sizeof(void *) * 24), -}; +// This array stores the deleted chains for smaller sizes, starting with +// sizeof(void *) and increasing in multiples thereof. +static const size_t num_small_deleted_chains = 24; +static DeletedBufferChain small_deleted_chains[num_small_deleted_chains] = {}; /** * Allocates the memory for a new buffer of the indicated size (which must be @@ -49,6 +27,8 @@ DeletedBufferChain DeletedBufferChain::_small_deleted_chains[DeletedBufferChain: */ void *DeletedBufferChain:: allocate(size_t size, TypeHandle type_handle) { + assert(_buffer_size > 0); + #ifdef USE_DELETED_CHAIN // TAU_PROFILE("void *DeletedBufferChain::allocate(size_t, TypeHandle)", " // ", TAU_USER); @@ -161,7 +141,18 @@ deallocate(void *ptr, TypeHandle type_handle) { * Returns a new DeletedBufferChain. */ DeletedBufferChain *DeletedBufferChain:: -get_large_deleted_chain(size_t buffer_size) { +get_deleted_chain(size_t buffer_size) { + // Common, smaller sized chains avoid the expensive locking and set + // manipulation code further down. + size_t index = ((buffer_size + sizeof(void *) - 1) / sizeof(void *)); + buffer_size = index * sizeof(void *); + index--; + if (index < num_small_deleted_chains) { + DeletedBufferChain *chain = &small_deleted_chains[index]; + chain->_buffer_size = buffer_size; + return chain; + } + static MutexImpl lock; lock.lock(); static std::set deleted_chains; diff --git a/dtool/src/dtoolbase/deletedBufferChain.h b/dtool/src/dtoolbase/deletedBufferChain.h index 25adf6ce5e..1e47adc81b 100644 --- a/dtool/src/dtoolbase/deletedBufferChain.h +++ b/dtool/src/dtoolbase/deletedBufferChain.h @@ -57,10 +57,9 @@ enum DeletedChainFlag : unsigned int { * Use MemoryHook to get a new DeletedBufferChain of a particular size. */ class EXPCL_DTOOL_DTOOLBASE DeletedBufferChain { -protected: - constexpr explicit DeletedBufferChain(size_t buffer_size); - public: + constexpr DeletedBufferChain() = default; + constexpr explicit DeletedBufferChain(size_t buffer_size) noexcept; INLINE DeletedBufferChain(DeletedBufferChain &&from) noexcept; INLINE DeletedBufferChain(const DeletedBufferChain ©); @@ -72,11 +71,9 @@ public: INLINE bool operator < (const DeletedBufferChain &other) const; - static INLINE DeletedBufferChain *get_deleted_chain(size_t buffer_size); + static DeletedBufferChain *get_deleted_chain(size_t buffer_size); private: - static DeletedBufferChain *get_large_deleted_chain(size_t buffer_size); - class ObjectNode { public: #ifdef USE_DELETEDCHAINFLAG @@ -99,7 +96,7 @@ private: ObjectNode *_deleted_chain = nullptr; MutexImpl _lock; - const size_t _buffer_size; + size_t _buffer_size = 0; #ifndef USE_DELETEDCHAINFLAG // Without DELETEDCHAINFLAG, we don't even store the _flag member at all. @@ -110,11 +107,6 @@ private: static const size_t flag_reserved_bytes = sizeof(AtomicAdjust::Integer); #endif // USE_DELETEDCHAINFLAG - // This array stores the deleted chains for smaller sizes, starting with - // sizeof(void *) and increasing in multiples thereof. - static const size_t num_small_deleted_chains = 24; - static DeletedBufferChain _small_deleted_chains[num_small_deleted_chains]; - friend class MemoryHook; }; From ae4151c9e1badc2a379cae246e2a8adc184ce6f8 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 9 Oct 2023 12:51:52 +0200 Subject: [PATCH 03/29] showbase: Fix import of Loader if AudioLoadRequest is not available --- direct/src/showbase/Loader.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/direct/src/showbase/Loader.py b/direct/src/showbase/Loader.py index 92ed0cf72f..3aad55647a 100644 --- a/direct/src/showbase/Loader.py +++ b/direct/src/showbase/Loader.py @@ -5,7 +5,6 @@ sound, music, shaders and fonts from disk. __all__ = ['Loader'] from panda3d.core import ( - AudioLoadRequest, ConfigVariableBool, Filename, FontPool, @@ -977,6 +976,8 @@ class Loader(DirectObject): just as in loadModel(); otherwise, the loading happens before loadSound() returns.""" + from panda3d.core import AudioLoadRequest + if not isinstance(soundPath, (tuple, list, set)): # We were given a single sound pathname or a MovieAudio instance. soundList = [soundPath] From dcc96a60b1a7a6259c1e8f50595b242359fb8501 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 9 Oct 2023 16:50:59 +0200 Subject: [PATCH 04/29] makepanda: Make version parsing in CreatePandaVersionFiles more robust --- makepanda/makepanda.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index 23fd57afaa..1f4ca17321 100755 --- a/makepanda/makepanda.py +++ b/makepanda/makepanda.py @@ -3038,11 +3038,18 @@ END #endif""" def CreatePandaVersionFiles(): - version1=int(VERSION.split(".")[0]) - version2=int(VERSION.split(".")[1]) - version3=int(VERSION.split(".")[2]) - nversion=version1*1000000+version2*1000+version3 - if (DISTRIBUTOR != "cmu"): + parts = VERSION.split(".", 2) + version1 = int(parts[0]) + version2 = int(parts[1]) + version3 = 0 + if len(parts) > 2: + for c in parts[2]: + if c.isdigit(): + version3 = version3 * 10 + ord(c) - 48 + else: + break + nversion = version1 * 1000000 + version2 * 1000 + version3 + if DISTRIBUTOR != "cmu": # Subtract 1 if we are not an official version. nversion -= 1 From a2fa54f385171e2efe8fec762c3e1230fbff0b6d Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 9 Oct 2023 16:52:45 +0200 Subject: [PATCH 05/29] gobj: fix `_contexts != nullptr` assert when prepare fails --- panda/src/gobj/geomVertexArrayData.cxx | 17 +++++++++++------ panda/src/gobj/shaderBuffer.cxx | 17 +++++++++++------ 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/panda/src/gobj/geomVertexArrayData.cxx b/panda/src/gobj/geomVertexArrayData.cxx index 75a9a5db20..674c2182b6 100644 --- a/panda/src/gobj/geomVertexArrayData.cxx +++ b/panda/src/gobj/geomVertexArrayData.cxx @@ -239,19 +239,24 @@ is_prepared(PreparedGraphicsObjects *prepared_objects) const { VertexBufferContext *GeomVertexArrayData:: prepare_now(PreparedGraphicsObjects *prepared_objects, GraphicsStateGuardianBase *gsg) { - if (_contexts == nullptr) { + if (_contexts != nullptr) { + Contexts::const_iterator ci; + ci = _contexts->find(prepared_objects); + if (ci != _contexts->end()) { + return (*ci).second; + } + } else { _contexts = new Contexts; } - Contexts::const_iterator ci; - ci = _contexts->find(prepared_objects); - if (ci != _contexts->end()) { - return (*ci).second; - } VertexBufferContext *vbc = prepared_objects->prepare_vertex_buffer_now(this, gsg); if (vbc != nullptr) { (*_contexts)[prepared_objects] = vbc; } + else if (_contexts->empty()) { + delete _contexts; + _contexts = nullptr; + } return vbc; } diff --git a/panda/src/gobj/shaderBuffer.cxx b/panda/src/gobj/shaderBuffer.cxx index fa16293d4d..9164a870ae 100644 --- a/panda/src/gobj/shaderBuffer.cxx +++ b/panda/src/gobj/shaderBuffer.cxx @@ -76,19 +76,24 @@ is_prepared(PreparedGraphicsObjects *prepared_objects) const { BufferContext *ShaderBuffer:: prepare_now(PreparedGraphicsObjects *prepared_objects, GraphicsStateGuardianBase *gsg) { - if (_contexts == nullptr) { + if (_contexts != nullptr) { + Contexts::const_iterator ci; + ci = _contexts->find(prepared_objects); + if (ci != _contexts->end()) { + return (*ci).second; + } + } else { _contexts = new Contexts; } - Contexts::const_iterator ci; - ci = _contexts->find(prepared_objects); - if (ci != _contexts->end()) { - return (*ci).second; - } BufferContext *vbc = prepared_objects->prepare_shader_buffer_now(this, gsg); if (vbc != nullptr) { (*_contexts)[prepared_objects] = vbc; } + else if (_contexts->empty()) { + delete _contexts; + _contexts = nullptr; + } return vbc; } From d5263b597b717606a44a3c2f2861d6458585f7bc Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 9 Oct 2023 17:02:33 +0200 Subject: [PATCH 06/29] makepanda: Strip version suffixes when parsing setup.cfg metadata Fixes #1539 --- makepanda/makepanda.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index 1f4ca17321..407c95bb33 100755 --- a/makepanda/makepanda.py +++ b/makepanda/makepanda.py @@ -421,6 +421,12 @@ if VERSION is None: else: # Take the value from the setup.cfg file. VERSION = GetMetadataValue('version') + match = re.match(r'^\d+\.\d+(\.\d+)+', VERSION) + if not match: + exit("Invalid version %s in setup.cfg, three digits are required" % (VERSION)) + if WHLVERSION is None: + WHLVERSION = VERSION + VERSION = match.group() if WHLVERSION is None: WHLVERSION = VERSION From b8ea78844015493d2fc5c64758276a821963e269 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 9 Oct 2023 17:44:36 +0200 Subject: [PATCH 07/29] display: Fix shadowViewMatrix regression for NodePath shader inputs Stopped working in ba388e28666e28b752d326ba0e680421db3c9bb1 --- panda/src/display/graphicsStateGuardian.cxx | 41 ++++++++++++++++----- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/panda/src/display/graphicsStateGuardian.cxx b/panda/src/display/graphicsStateGuardian.cxx index e29d48646d..feb017f382 100644 --- a/panda/src/display/graphicsStateGuardian.cxx +++ b/panda/src/display/graphicsStateGuardian.cxx @@ -64,6 +64,11 @@ using std::string; +static const LMatrix4 shadow_bias_mat(0.5f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.5f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.5f, 0.0f, + 0.5f, 0.5f, 0.5f, 1.0f); + //PStatCollector GraphicsStateGuardian::_vertex_buffer_switch_pcollector("Buffer switch:Vertex"); //PStatCollector GraphicsStateGuardian::_index_buffer_switch_pcollector("Buffer switch:Index"); //PStatCollector GraphicsStateGuardian::_shader_buffer_switch_pcollector("Buffer switch:Shader"); @@ -1543,9 +1548,30 @@ fetch_specified_part(Shader::ShaderMatInput part, InternalName *name, } const NodePath &np = _target_shader->get_shader_input_nodepath(name->get_parent()); - nassertv(!np.is_empty()); + const PandaNode *node = np.node(); - fetch_specified_member(np, name->get_basename(), into[0]); + // This is the only matrix member we support from NodePath inputs. + if (node != nullptr && node->is_of_type(LensNode::get_class_type()) && + name->get_basename() == "shadowViewMatrix") { + const LensNode *lnode = (const LensNode *)node; + const Lens *lens = lnode->get_lens(); + + LMatrix4 t = _inv_cs_transform->get_mat() * + _scene_setup->get_camera_transform()->get_mat() * + np.get_net_transform()->get_inverse()->get_mat() * + LMatrix4::convert_mat(_coordinate_system, lens->get_coordinate_system()); + + if (!lnode->is_of_type(PointLight::get_class_type())) { + t *= lens->get_projection_mat() * shadow_bias_mat; + } + *(LMatrix4f *)into = LCAST(float, t); + } + else { + display_cat.error() + << "Shader input " << *name << " requests invalid attribute " + << name->get_basename() << " from node " << np << "\n"; + *(LMatrix4f *)into = LMatrix4f::ident_mat(); + } return; } case Shader::SMO_vec_constant_x_attrib: { @@ -1588,12 +1614,7 @@ fetch_specified_part(Shader::ShaderMatInput part, InternalName *name, } return; } - case Shader::SMO_apiview_to_apiclip_light_source_i: { - static const LMatrix4 biasmat(0.5f, 0.0f, 0.0f, 0.0f, - 0.0f, 0.5f, 0.0f, 0.0f, - 0.0f, 0.0f, 0.5f, 0.0f, - 0.5f, 0.5f, 0.5f, 1.0f); - + case Shader::SMO_apiview_to_apiclip_light_source_i: { // shadowViewMatrix const LightAttrib *target_light; _target_rs->get_attrib_def(target_light); @@ -1616,14 +1637,14 @@ fetch_specified_part(Shader::ShaderMatInput part, InternalName *name, LMatrix4::convert_mat(_coordinate_system, lens->get_coordinate_system()); if (!lnode->is_of_type(PointLight::get_class_type())) { - t *= lens->get_projection_mat() * biasmat; + t *= lens->get_projection_mat() * shadow_bias_mat; } ((LMatrix4f *)into)[i] = LCAST(float, t); } // Apply just the bias matrix otherwise. for (; i < (size_t)count; ++i) { - ((LMatrix4f *)into)[i] = LCAST(float, biasmat); + ((LMatrix4f *)into)[i] = LCAST(float, shadow_bias_mat); } return; } From ef25b67ddbbcae540d257ef98e1cf3bbc4977a2f Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 10 Oct 2023 12:45:21 +0200 Subject: [PATCH 08/29] interval: Remove unrunnable IntervalTest module --- direct/src/interval/FunctionInterval.py | 6 - direct/src/interval/IntervalTest.py | 222 ------------------------ 2 files changed, 228 deletions(-) delete mode 100644 direct/src/interval/IntervalTest.py diff --git a/direct/src/interval/FunctionInterval.py b/direct/src/interval/FunctionInterval.py index 60c0ab7edf..fac7cb2b0d 100644 --- a/direct/src/interval/FunctionInterval.py +++ b/direct/src/interval/FunctionInterval.py @@ -10,12 +10,6 @@ from direct.directnotify.DirectNotifyGlobal import directNotify from . import Interval -############################################################# -### ### -### See examples of function intervals in IntervalTest.py ### -### ### -############################################################# - class FunctionInterval(Interval.Interval): # Name counter functionIntervalNum = 1 diff --git a/direct/src/interval/IntervalTest.py b/direct/src/interval/IntervalTest.py deleted file mode 100644 index 8705f70581..0000000000 --- a/direct/src/interval/IntervalTest.py +++ /dev/null @@ -1,222 +0,0 @@ -"""Undocumented Module""" - -__all__ = () - - -if __name__ == "__main__": - from panda3d.core import Filename, Point3, Vec3 - from direct.showbase.DirectObject import DirectObject - from direct.showbase.ShowBase import ShowBase - from direct.actor.Actor import Actor - from direct.directutil import Mopath - from direct.showbase.MessengerGlobal import messenger - from .ActorInterval import ActorInterval - from .FunctionInterval import ( - AcceptInterval, - EventInterval, - FunctionInterval, - IgnoreInterval, - PosHprInterval, - ) - from .LerpInterval import LerpPosInterval, LerpHprInterval, LerpPosHprInterval - from .MopathInterval import MopathInterval - from .SoundInterval import SoundInterval - from .MetaInterval import PREVIOUS_END, PREVIOUS_START, TRACK_START, Track - - base = ShowBase() - - boat = base.loader.loadModel('models/misc/smiley') - boat.reparentTo(base.render) - - donald = Actor() - donald.loadModel("phase_6/models/char/donald-wheel-1000") - donald.loadAnims({"steer":"phase_6/models/char/donald-wheel-wheel"}) - donald.reparentTo(boat) - - dock = base.loader.loadModel('models/misc/smiley') - dock.reparentTo(base.render) - - sound = base.loader.loadSfx('phase_6/audio/sfx/SZ_DD_waterlap.mp3') - foghorn = base.loader.loadSfx('phase_6/audio/sfx/SZ_DD_foghorn.mp3') - - mp = Mopath.Mopath() - mp.loadFile(Filename('phase_6/paths/dd-e-w')) - - # Set up the boat - boatMopath = MopathInterval(mp, boat, 'boatpath') - boatTrack = Track([boatMopath], 'boattrack') - BOAT_START = boatTrack.getIntervalStartTime('boatpath') - BOAT_END = boatTrack.getIntervalEndTime('boatpath') - - # This will create an anim interval that is posed every frame - donaldSteerInterval = ActorInterval(donald, 'steer') - # This will create an anim interval that is started at t = 0 and then - # loops for 10 seconds - donaldLoopInterval = ActorInterval(donald, 'steer', loop=1, duration = 10.0) - donaldSteerTrack = Track([donaldSteerInterval, donaldLoopInterval], - name = 'steerTrack') - - # Make the dock lerp up so that it's up when the boat reaches the end of - # its mopath - dockLerp = LerpPosHprInterval(dock, 5.0, - pos=Point3(0, 0, -5), - hpr=Vec3(0, 0, 0), - name='dock-lerp') - # We need the dock's state to be defined before the lerp - dockPos = PosHprInterval(dock, dock.getPos(), dock.getHpr(), 1.0, 'dockpos') - dockUpTime = BOAT_END - dockLerp.getDuration() - hpr2 = Vec3(90.0, 90.0, 90.0) - dockLerp2 = LerpHprInterval(dock, 3.0, hpr2, name='hpr-lerp') - dockTrack = Track([dockLerp2, dockPos, dockLerp], 'docktrack') - dockTrack.setIntervalStartTime('dock-lerp', dockUpTime) - dockTrack.setIntervalStartTime('hpr-lerp', BOAT_START) - - # Start the water sound 5 seconds after the boat starts moving - waterStartTime = BOAT_START + 5.0 - waterSound = SoundInterval(sound, name='watersound') - soundTrack = Track([waterSound], 'soundtrack') - soundTrack.setIntervalStartTime('watersound', waterStartTime) - - # Throw an event when the water track ends - eventTime = soundTrack.getIntervalEndTime('watersound') - waterDone = EventInterval('water-is-done') - waterEventTrack = Track([waterDone]) - waterEventTrack.setIntervalStartTime('water-is-done', eventTime) - - def handleWaterDone(): - print('water is done') - - # Interval can handle its own event - messenger.accept('water-is-done', waterDone, handleWaterDone) - - foghornStartTime = BOAT_START + 4.0 - foghornSound = SoundInterval(foghorn, name='foghorn') - soundTrack2 = Track([(foghornStartTime, foghornSound)], 'soundtrack2') - - mtrack = MultiTrack([boatTrack, dockTrack, soundTrack, soundTrack2, waterEventTrack, # type: ignore[name-defined] - donaldSteerTrack]) - # Print out MultiTrack parameters - print(mtrack) - - ### Using lambdas and functions ### - # Using a lambda - i1 = FunctionInterval(lambda: base.transitions.fadeOut()) - i2 = FunctionInterval(lambda: base.transitions.fadeIn()) - - def caughtIt(): - print('Caught here-is-an-event') - - class DummyAcceptor(DirectObject): - pass - - da = DummyAcceptor() - i3 = AcceptInterval(da, 'here-is-an-event', caughtIt) - - i4 = EventInterval('here-is-an-event') - - i5 = IgnoreInterval(da, 'here-is-an-event') - - # Using a function - def printDone(): - print('done') - - i6 = FunctionInterval(printDone) - - # Create track - t1 = Track([ - # Fade out - (0.0, i1), - # Fade in - (2.0, i2), - # Accept event - (4.0, i3), - # Throw it, - (5.0, i4), - # Ignore event - (6.0, i5), - # Throw event again and see if ignore worked - (7.0, i4), - # Print done - (8.0, i6)], name = 'demo') - - print(t1) - - ### Specifying interval start times during track construction ### - # Interval start time can be specified relative to three different points: - # PREVIOUS_END - # PREVIOUS_START - # TRACK_START - - startTime = 0.0 - def printStart(): - global startTime - startTime = base.clock.getFrameTime() - print('Start') - - def printPreviousStart(): - global startTime - currTime = base.clock.getFrameTime() - print('PREVIOUS_END %0.2f' % (currTime - startTime)) - - def printPreviousEnd(): - global startTime - currTime = base.clock.getFrameTime() - print('PREVIOUS_END %0.2f' % (currTime - startTime)) - - def printTrackStart(): - global startTime - currTime = base.clock.getFrameTime() - print('TRACK_START %0.2f' % (currTime - startTime)) - - def printArguments(a, b, c): - print('My args were %d, %d, %d' % (a, b, c)) - - i1 = FunctionInterval(printStart) - # Just to take time - i2 = LerpPosInterval(base.camera, 2.0, Point3(0, 10, 5)) - # This will be relative to end of camera move - i3 = FunctionInterval(printPreviousEnd) # type: ignore[assignment] - # Just to take time - i4 = LerpPosInterval(base.camera, 2.0, Point3(0, 0, 5)) - # This will be relative to the start of the camera move - i5 = FunctionInterval(printPreviousStart) # type: ignore[assignment] - # This will be relative to track start - i6 = FunctionInterval(printTrackStart) - # This will print some arguments - # This will be relative to track start - i7 = FunctionInterval(printArguments, extraArgs = [1, 10, 100]) - # Create the track, if you don't specify offset type in tuple it defaults to - # relative to TRACK_START (first entry below) - t2 = Track([(0.0, i1), # i1 start at t = 0, duration = 0.0 - (1.0, i2, TRACK_START), # i2 start at t = 1, duration = 2.0 - (2.0, i3, PREVIOUS_END), # i3 start at t = 5, duration = 0.0 - (1.0, i4, PREVIOUS_END), # i4 start at t = 6, duration = 2.0 - (3.0, i5, PREVIOUS_START), # i5 start at t = 9, duration = 0.0 - (10.0, i6, TRACK_START), # i6 start at t = 10, duration = 0.0 - (12.0, i7)], # i7 start at t = 12, duration = 0.0 - name = 'startTimeDemo') - - print(t2) - - # Play tracks - # mtrack.play() - # t1.play() - # t2.play() - - - def test(n): - lerps = [] - for i in range(n): - lerps.append(LerpPosHprInterval(dock, 5.0, - pos=Point3(0, 0, -5), - hpr=Vec3(0, 0, 0), - startPos=dock.getPos(), - startHpr=dock.getHpr(), - name='dock-lerp')) - lerps.append(EventInterval("joe")) - t = Track(lerps) - mt = MultiTrack([t]) - # return mt - - test(5) - base.run() From f7718b466bbf1b06d0c2a042f91896153b14c5a5 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 10 Oct 2023 12:52:26 +0200 Subject: [PATCH 09/29] direct: Fix assorted issues when using mypyc --- direct/src/directnotify/Notifier.py | 4 ++-- direct/src/gui/DirectEntry.py | 2 +- direct/src/gui/OnscreenText.py | 2 +- direct/src/showbase/ShowBase.py | 22 ++++++++++++---------- direct/src/showbase/ShowBaseGlobal.py | 2 +- direct/src/showutil/TexViewer.py | 2 +- direct/src/task/Task.py | 9 ++++++++- 7 files changed, 26 insertions(+), 17 deletions(-) diff --git a/direct/src/directnotify/Notifier.py b/direct/src/directnotify/Notifier.py index bf3f14779a..cff41aa908 100644 --- a/direct/src/directnotify/Notifier.py +++ b/direct/src/directnotify/Notifier.py @@ -256,7 +256,7 @@ class Notifier: the function call (with parameters). """ #f.f_locals['self'].__init__.im_class.__name__ - if self.__debug: + if __debug__ and self.__debug: state = '' doId = '' if obj is not None: @@ -296,7 +296,7 @@ class Notifier: call followed by the notifier category and the function call (with parameters). """ - if self.__debug: + if __debug__ and self.__debug: message = str(debugString) string = ":%s:%s \"%s\" %s"%( self.getOnlyTime(), diff --git a/direct/src/gui/DirectEntry.py b/direct/src/gui/DirectEntry.py index f106e40aad..632ba35738 100644 --- a/direct/src/gui/DirectEntry.py +++ b/direct/src/gui/DirectEntry.py @@ -28,7 +28,7 @@ class DirectEntry(DirectFrame): to keyboard buttons """ - directWtext = ConfigVariableBool('direct-wtext', 1) + directWtext = ConfigVariableBool('direct-wtext', True) AllowCapNamePrefixes = ("Al", "Ap", "Ben", "De", "Del", "Della", "Delle", "Der", "Di", "Du", "El", "Fitz", "La", "Las", "Le", "Les", "Lo", "Los", diff --git a/direct/src/gui/OnscreenText.py b/direct/src/gui/OnscreenText.py index e3a8912151..4050f725f8 100644 --- a/direct/src/gui/OnscreenText.py +++ b/direct/src/gui/OnscreenText.py @@ -173,7 +173,7 @@ class OnscreenText(NodePath): self.__wordwrap = wordwrap if decal: - textNode.setCardDecal(1) + textNode.setCardDecal(True) if font is None: font = DGG.getDefaultFont() diff --git a/direct/src/showbase/ShowBase.py b/direct/src/showbase/ShowBase.py index 1eb2c9aa00..df2630c636 100644 --- a/direct/src/showbase/ShowBase.py +++ b/direct/src/showbase/ShowBase.py @@ -123,6 +123,7 @@ import builtins builtins.config = DConfig # type: ignore[attr-defined] from direct.directnotify.DirectNotifyGlobal import directNotify, giveNotify +from direct.directnotify.Notifier import Notifier from .MessengerGlobal import messenger from .BulletinBoardGlobal import bulletinBoard from direct.task.TaskManagerGlobal import taskMgr @@ -140,6 +141,7 @@ import importlib from direct.showbase import ExceptionVarDump from . import DirectObject from . import SfxPlayer +from typing import ClassVar if __debug__: from direct.showbase import GarbageReport from direct.directutil import DeltaProfiler @@ -160,8 +162,9 @@ def exitfunc(): class ShowBase(DirectObject.DirectObject): #: The deprecated `.DConfig` interface for accessing config variables. - config = DConfig - notify = directNotify.newCategory("ShowBase") + config: ClassVar = DConfig + notify: ClassVar[Notifier] = directNotify.newCategory("ShowBase") + guiItems: ClassVar[dict] def __init__(self, fStartDirect=True, windowType=None): """Opens a window, sets up a 3-D and several 2-D scene graphs, and @@ -337,10 +340,10 @@ class ShowBase(DirectObject.DirectObject): self.tkRootCreated = False # This is used for syncing multiple PCs in a distributed cluster - try: + if hasattr(builtins, 'clusterSyncFlag'): # Has the cluster sync variable been set externally? - self.clusterSyncFlag = clusterSyncFlag - except NameError: + self.clusterSyncFlag = builtins.clusterSyncFlag + else: # Has the clusterSyncFlag been set via a config variable self.clusterSyncFlag = ConfigVariableBool('cluster-sync', False) @@ -712,10 +715,9 @@ class ShowBase(DirectObject.DirectObject): except Exception: pass - if hasattr(self, 'win'): - del self.win - del self.winList - del self.pipe + self.win = None + self.winList.clear() + self.pipe = None def makeDefaultPipe(self, printPipeTypes = None): """ @@ -728,7 +730,7 @@ class ShowBase(DirectObject.DirectObject): # When the user didn't specify an explicit setting, take the value # from the config variable. We could just omit the parameter, however # this way we can keep backward compatibility. - printPipeTypes = ConfigVariableBool("print-pipe-types", True) + printPipeTypes = ConfigVariableBool("print-pipe-types", True).value selection = GraphicsPipeSelection.getGlobalPtr() if printPipeTypes: diff --git a/direct/src/showbase/ShowBaseGlobal.py b/direct/src/showbase/ShowBaseGlobal.py index b24fca18fc..58e7e69f58 100644 --- a/direct/src/showbase/ShowBaseGlobal.py +++ b/direct/src/showbase/ShowBaseGlobal.py @@ -21,7 +21,7 @@ from panda3d.core import NodePath, PGTop from . import DConfig as config # pylint: disable=unused-import import warnings -__dev__ = ConfigVariableBool('want-dev', __debug__).value +__dev__: bool = ConfigVariableBool('want-dev', __debug__).value base: ShowBase diff --git a/direct/src/showutil/TexViewer.py b/direct/src/showutil/TexViewer.py index 32b181b22a..5ad823b19f 100644 --- a/direct/src/showutil/TexViewer.py +++ b/direct/src/showutil/TexViewer.py @@ -18,7 +18,7 @@ class TexViewer(DirectObject): # We'll put the full-resolution texture on the left. cm = CardMaker('left') - l, r, b, t = (-1, -0.1, 0, 0.9) + l, r, b, t = (-1.0, -0.1, 0.0, 0.9) cm.setFrame(l, r, b, t) left = cards.attachNewNode(cm.generate()) left.setTexture(self.tex) diff --git a/direct/src/task/Task.py b/direct/src/task/Task.py index 12716a6707..b0aa610e40 100644 --- a/direct/src/task/Task.py +++ b/direct/src/task/Task.py @@ -13,6 +13,7 @@ __all__ = ['Task', 'TaskManager', from direct.directnotify.DirectNotifyGlobal import directNotify from direct.showbase.PythonUtil import Functor, ScratchPad from direct.showbase.MessengerGlobal import messenger +from typing import Any, Optional import types import random import importlib @@ -20,6 +21,7 @@ import sys # On Android, there's no use handling SIGINT, and in fact we can't, since we # run the application in a separate thread from the main thread. +signal: Optional[types.ModuleType] if hasattr(sys, 'getandroidapilevel'): signal = None else: @@ -140,6 +142,8 @@ class TaskManager: MaxEpochSpeed = 1.0/30.0 + __prevHandler: Any + def __init__(self): self.mgr = AsyncTaskManager.getGlobalPtr() @@ -183,11 +187,14 @@ class TaskManager: self._frameProfileQueue.clear() self.mgr.cleanup() + def __getClock(self): + return self.mgr.getClock() + def setClock(self, clockObject): self.mgr.setClock(clockObject) self.globalClock = clockObject - clock = property(lambda self: self.mgr.getClock(), setClock) + clock = property(__getClock, setClock) def invokeDefaultHandler(self, signalNumber, stackFrame): print('*** allowing mid-frame keyboard interrupt.') From e55bb94996691e560cbb8f0bb55fe71cf9f65369 Mon Sep 17 00:00:00 2001 From: Kylie Smith Date: Tue, 10 Oct 2023 10:02:24 +1000 Subject: [PATCH 10/29] Added missing control message types --- direct/src/distributed/MsgTypes.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/direct/src/distributed/MsgTypes.py b/direct/src/distributed/MsgTypes.py index 3e0a204a0c..c946a12951 100644 --- a/direct/src/distributed/MsgTypes.py +++ b/direct/src/distributed/MsgTypes.py @@ -42,6 +42,9 @@ CONTROL_ADD_RANGE = 9002 CONTROL_REMOVE_RANGE = 9003 CONTROL_ADD_POST_REMOVE = 9010 CONTROL_CLEAR_POST_REMOVES = 9011 +CONTROL_SET_CON_NAME = 9012 +CONTROL_SET_CON_URL = 9013 +CONTROL_LOG_MESSAGE = 9014 # State Server control messages: STATESERVER_CREATE_OBJECT_WITH_REQUIRED = 2000 From ae3cbe4b121c453582648d1ea5eefc1cd16474d9 Mon Sep 17 00:00:00 2001 From: LD <44778133+el-dee@users.noreply.github.com> Date: Tue, 10 Oct 2023 16:56:04 +0200 Subject: [PATCH 11/29] cocoadisplay: Add support for high-dpi screens (#1308) --- direct/src/dist/commands.py | 1 + panda/src/cocoadisplay/cocoaGraphicsPipe.mm | 51 ++- panda/src/cocoadisplay/cocoaGraphicsWindow.h | 1 + panda/src/cocoadisplay/cocoaGraphicsWindow.mm | 314 ++++++++++++------ .../cocoadisplay/cocoaPandaWindowDelegate.h | 1 + .../cocoadisplay/cocoaPandaWindowDelegate.mm | 4 + panda/src/cocoadisplay/config_cocoadisplay.h | 1 + panda/src/cocoadisplay/config_cocoadisplay.mm | 5 + 8 files changed, 268 insertions(+), 110 deletions(-) diff --git a/direct/src/dist/commands.py b/direct/src/dist/commands.py index 5410199307..3bfafe77c3 100644 --- a/direct/src/dist/commands.py +++ b/direct/src/dist/commands.py @@ -717,6 +717,7 @@ class build_apps(setuptools.Command): 'CFBundlePackageType': 'APPL', 'CFBundleSignature': '', #TODO 'CFBundleExecutable': self.macos_main_app, + 'NSHighResolutionCapable': 'True', } icon = self.icon_objects.get( diff --git a/panda/src/cocoadisplay/cocoaGraphicsPipe.mm b/panda/src/cocoadisplay/cocoaGraphicsPipe.mm index 0ece9f83a2..83a487e9fd 100644 --- a/panda/src/cocoadisplay/cocoaGraphicsPipe.mm +++ b/panda/src/cocoadisplay/cocoaGraphicsPipe.mm @@ -40,12 +40,36 @@ CocoaGraphicsPipe(CGDirectDisplayID display) : _display(display) { [thread start]; [thread autorelease]; + // If the application is dpi-aware, iterate over all the screens to find the + // one with our display ID and get the backing scale factor to configure the + // detected display zoom. Otherwise the detected display zoom keeps its + // default value of 1.0 + + if (dpi_aware) { + NSScreen *screen; + NSEnumerator *e = [[NSScreen screens] objectEnumerator]; + while (screen = (NSScreen *) [e nextObject]) { + NSNumber *num = [[screen deviceDescription] objectForKey: @"NSScreenNumber"]; + if (_display == (CGDirectDisplayID) [num longValue]) { + set_detected_display_zoom([screen backingScaleFactor]); + if (cocoadisplay_cat.is_debug()) { + cocoadisplay_cat.debug() + << "Display zoom is " << [screen backingScaleFactor] << "\n"; + } + break; + } + } + } + // We used to also obtain the corresponding NSScreen here, but this causes // the application icon to start bouncing, which may be undesirable for // apps that will never open a window. - _display_width = CGDisplayPixelsWide(_display); - _display_height = CGDisplayPixelsHigh(_display); + // Although the name of these functions mention pixels, they actually return + // display points, we use the detected display zoom to transform the values + // into pixels. + _display_width = CGDisplayPixelsWide(_display) * _detected_display_zoom; + _display_height = CGDisplayPixelsHigh(_display) * _detected_display_zoom; load_display_information(); if (cocoadisplay_cat.is_debug()) { @@ -64,19 +88,36 @@ load_display_information() { // _display_information->_device_id = CGDisplaySerialNumber(_display); // Display modes + CFDictionaryRef options = NULL; + const CFStringRef dictkeys[] = {kCGDisplayShowDuplicateLowResolutionModes}; + const CFBooleanRef dictvalues[] = {kCFBooleanTrue}; + options = CFDictionaryCreate(NULL, + (const void **)dictkeys, + (const void **)dictvalues, + 1, + &kCFCopyStringDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks); size_t num_modes = 0; - CFArrayRef modes = CGDisplayCopyAllDisplayModes(_display, NULL); + CFArrayRef modes = CGDisplayCopyAllDisplayModes(_display, options); if (modes != NULL) { num_modes = CFArrayGetCount(modes); _display_information->_total_display_modes = num_modes; _display_information->_display_mode_array = new DisplayMode[num_modes]; } + if (options != NULL) { + CFRelease(options); + } for (size_t i = 0; i < num_modes; ++i) { CGDisplayModeRef mode = (CGDisplayModeRef) CFArrayGetValueAtIndex(modes, i); - _display_information->_display_mode_array[i].width = CGDisplayModeGetWidth(mode); - _display_information->_display_mode_array[i].height = CGDisplayModeGetHeight(mode); + if (dpi_aware) { + _display_information->_display_mode_array[i].width = CGDisplayModeGetPixelWidth(mode); + _display_information->_display_mode_array[i].height = CGDisplayModeGetPixelHeight(mode); + } else { + _display_information->_display_mode_array[i].width = CGDisplayModeGetWidth(mode); + _display_information->_display_mode_array[i].height = CGDisplayModeGetHeight(mode); + } _display_information->_display_mode_array[i].refresh_rate = CGDisplayModeGetRefreshRate(mode); _display_information->_display_mode_array[i].fullscreen_only = false; diff --git a/panda/src/cocoadisplay/cocoaGraphicsWindow.h b/panda/src/cocoadisplay/cocoaGraphicsWindow.h index 19b9b6004a..e555489d24 100644 --- a/panda/src/cocoadisplay/cocoaGraphicsWindow.h +++ b/panda/src/cocoadisplay/cocoaGraphicsWindow.h @@ -63,6 +63,7 @@ public: void handle_minimize_event(bool minimized); void handle_maximize_event(bool maximized); void handle_foreground_event(bool foreground); + void handle_backing_change_event(); bool handle_close_request(); void handle_close_event(); void handle_key_event(NSEvent *event); diff --git a/panda/src/cocoadisplay/cocoaGraphicsWindow.mm b/panda/src/cocoadisplay/cocoaGraphicsWindow.mm index d212c84ebb..e76cf38fa0 100644 --- a/panda/src/cocoadisplay/cocoaGraphicsWindow.mm +++ b/panda/src/cocoadisplay/cocoaGraphicsWindow.mm @@ -132,13 +132,20 @@ move_pointer(int device, int x, int y) { return true; } + // Mouse position is expressed in screen points and not pixels, but in Panda3D + // we are using pixel coordinates. + // Instead of using convertPointFromBacking and have complex logic to cope with + // the change of coordinate system, we cheat and directly use the contents scale + // of the view layer to convert pixel coordinates into screen point coordinates. + CGFloat contents_scale = _view.layer.contentsScale; if (device == 0) { CGPoint point; if (_properties.get_fullscreen()) { - point = CGPointMake(x, y); + point = CGPointMake(float(x) / contents_scale, + float(y) / contents_scale); } else { - point = CGPointMake(x + _properties.get_x_origin(), - y + _properties.get_y_origin()); + point = CGPointMake((float(x) + _properties.get_x_origin()) / contents_scale, + (float(y) + _properties.get_y_origin()) / contents_scale); } if (CGWarpMouseCursorPosition(point) == kCGErrorSuccess) { @@ -302,35 +309,89 @@ open_window() { } } + // Configure the origin and the size of the window. + // On macOS, screen coordinates are expressed in "points" which are independant + // of the pixel density of the screen. Panda3D however, expresses the size and + // origin of a window in pixels. + // So, when opening a window, we need to convert the origin and size from pixel + // units into point units. However, this conversion depends on the pixel density + // of the screen, the backing scale factor. + // As the origin and size of a window depends on the size of the screen of the + // parent view, their size must be converted first from points to pixels. + // If a window (or a view) is not configured to support high-dpi screen, macOS + // will upscale the window (or view) when displayed on a high-dpi screen. + // Therefore its backing scale factor will always be 1.0 + // In a Panda3D application, windows are always configured as high resolution + // capable, but the view is only configured as high resolution if the dpi-aware + // configuration flag is set. + // If the app is not dpi-aware, we must upscale its size and origin from points + // into pixels as the window is always high resolution capable. + // Center the window if coordinates were set to -1 or -2 TODO: perhaps in // future, in the case of -1, it should use the origin used in a previous // run of Panda + + // Size of the requested window + NSSize size = NSMakeSize(_properties.get_x_size(), _properties.get_y_size()); NSRect container; + CGFloat backing_scale_factor = screen.backingScaleFactor; if (parent_nsview != NULL) { - container = [parent_nsview bounds]; + // Convert parent view bounds into pixel units. + container = [parent_nsview convertRectToBacking:[parent_nsview bounds]]; + // If the app is not dpi-aware, we must convert its size from points into + // pixels as the window is always high resolution capable + if (!dpi_aware) { + size = [parent_nsview convertSizeToBacking:size]; + } } else { container = [screen frame]; container.origin = NSMakePoint(0, 0); + container = [screen convertRectToBacking:container]; + if (!dpi_aware) { + // Weirdly NSScreen does not respond to convertSizeToBacking, so we have to + // create a dummy rect just for converting the window size. + NSRect rect; + rect.origin = NSMakePoint(0, 0); + rect.size = size; + rect = [screen convertRectToBacking:rect]; + size = rect.size; + } } int x = _properties.get_x_origin(); int y = _properties.get_y_origin(); + // As we are converting a single value and the view is not created yet, it's + // easier to simply use the backing scale factor and don't bother with + // coordinate system transformations. if (x < 0) { - x = floor(container.size.width / 2 - _properties.get_x_size() / 2); + x = floor(container.size.width / 2 - size.width / 2); + } else if (!dpi_aware) { + x *= backing_scale_factor; } if (y < 0) { - y = floor(container.size.height / 2 - _properties.get_y_size() / 2); + y = floor(container.size.height / 2 - size.height / 2); + } else if (!dpi_aware) { + y *= backing_scale_factor; + } + if (dpi_aware) { + _properties.set_origin(x, y); + } else { + _properties.set_origin(x / backing_scale_factor, y / backing_scale_factor); } - _properties.set_origin(x, y); if (_parent_window_handle == (WindowHandle *)NULL) { // Content rectangle NSRect rect; if (_properties.get_fullscreen()) { - rect = container; + rect = [screen convertRectFromBacking:container]; } else { - rect = NSMakeRect(x, container.size.height - _properties.get_y_size() - y, - _properties.get_x_size(), _properties.get_y_size()); + rect = NSMakeRect(x, container.size.height - size.height - y, + size.width, size.height); + if (parent_nsview != NULL) { + rect = [parent_nsview convertRectFromBacking:rect]; + } else { + rect = [screen convertRectFromBacking:rect]; + } } // Configure the window decorations @@ -388,8 +449,10 @@ open_window() { _parent_window_handle->attach_child(_window_handle); } - // Always disable application HiDPI support, Cocoa will do the eventual upscaling for us. - [_view setWantsBestResolutionOpenGLSurface:NO]; + // Configure the view to be high resolution capable using the dpi-aware + // configuration flag. If dpi-aware is false, macOS will upscale the view + // for us. + [_view setWantsBestResolutionOpenGLSurface:dpi_aware]; if (_properties.has_icon_filename()) { NSImage *image = load_image(_properties.get_icon_filename()); if (image != nil) { @@ -596,22 +659,6 @@ set_properties_now(WindowProperties &properties) { } if (switched) { - if (_window != nil) { - // For some reason, setting the style mask makes it give up its - // first-responder status. And for some reason, we need to first - // restore the window to normal level before we switch fullscreen, - // otherwise we may get a black bar if we're currently on Z_top. - if (_properties.get_z_order() != WindowProperties::Z_normal) { - [_window setLevel: NSNormalWindowLevel]; - } - if ([_window respondsToSelector:@selector(setStyleMask:)]) { - [_window setStyleMask:NSBorderlessWindowMask]; - } - [_window makeFirstResponder:_view]; - [_window setLevel:CGShieldingWindowLevel()]; - [_window makeKeyAndOrderFront:nil]; - } - // We've already set the size property this way; clear it. properties.clear_size(); _properties.set_size(width, height); @@ -684,6 +731,9 @@ set_properties_now(WindowProperties &properties) { NSMiniaturizableWindowMask | NSResizableWindowMask ]; } [_window makeFirstResponder:_view]; + // Resize event fired by makeFirstResponder has an invalid backing scale factor + // The actual size must be reset afterward + handle_resize_event(); } } @@ -705,6 +755,9 @@ set_properties_now(WindowProperties &properties) { NSMiniaturizableWindowMask | NSResizableWindowMask ]; } [_window makeFirstResponder:_view]; + // Resize event fired by makeFirstResponder has an invalid backing scale factor + // The actual size must be reset afterward + handle_resize_event(); } properties.clear_undecorated(); @@ -715,10 +768,13 @@ set_properties_now(WindowProperties &properties) { int height = properties.get_y_size(); if (!_properties.get_fullscreen()) { + // We use the view, not the window, to convert the frame size, expressed + // in pixels, into points as the "dpi awareness" is managed by the view. + NSSize size = [_view convertSizeFromBacking:NSMakeSize(width, height)]; if (_window != nil) { - [_window setContentSize:NSMakeSize(width, height)]; + [_window setContentSize:size]; } - [_view setFrameSize:NSMakeSize(width, height)]; + [_view setFrameSize:size]; if (cocoadisplay_cat.is_debug()) { cocoadisplay_cat.debug() @@ -768,12 +824,14 @@ set_properties_now(WindowProperties &properties) { // Get the frame for the screen NSRect frame; NSRect container; + // Note again that we are using the view to convert the frame and container + // size from points into pixels. if (_window != nil) { NSRect window_frame = [_window frame]; - frame = [_window contentRectForFrameRect:window_frame]; + frame = [_view convertRectToBacking:[_window contentRectForFrameRect:window_frame]]; NSScreen *screen = [_window screen]; nassertv(screen != nil); - container = [screen frame]; + container = [_view convertRectToBacking:[screen frame]]; // Prevent the centering from overlapping the Dock if (y < 0) { @@ -783,8 +841,8 @@ set_properties_now(WindowProperties &properties) { } } } else { - frame = [_view frame]; - container = [[_view superview] frame]; + frame = [_view convertRectToBacking:[_view frame]]; + container = [[_view superview] convertRectToBacking:[[_view superview] frame]]; } if (x < 0) { @@ -795,22 +853,22 @@ set_properties_now(WindowProperties &properties) { } _properties.set_origin(x, y); - if (!_properties.get_fullscreen()) { - // Remember, Mac OS X coordinates are flipped in the vertical axis. - frame.origin.x = x; - frame.origin.y = container.size.height - y - frame.size.height; + frame.origin.x = x; + // Y coordinate in backing store is not flipped, but origin is still at the bottom left + frame.origin.y = y - container.size.height; - if (cocoadisplay_cat.is_debug()) { - cocoadisplay_cat.debug() - << "Setting window content origin to " - << frame.origin.x << ", " << frame.origin.y << "\n"; - } + if (cocoadisplay_cat.is_debug()) { + cocoadisplay_cat.debug() + << "Setting window content origin to " + << frame.origin.x << ", " << frame.origin.y << "\n"; + } - if (_window != nil) { - [_window setFrame:[_window frameRectForContentRect:frame] display:NO]; - } else { - [_view setFrame:frame]; - } + if (_window != nil) { + frame = [_view convertRectFromBacking:frame]; + [_window setFrame:[_window frameRectForContentRect:frame] display:NO]; + } else { + frame = [_view convertRectFromBacking:frame]; + [_view setFrame:frame]; } properties.clear_origin(); } @@ -957,24 +1015,20 @@ unbind_context() { CFMutableArrayRef CocoaGraphicsWindow:: find_display_modes(int width, int height) { CFDictionaryRef options = NULL; - // On macOS 10.15+ (Catalina), we want to select the display mode with the - // samescaling factor as the current view to avoid cropping or scaling issues. - // This is a workaround until HiDPI display or scaling factor is properly - // handled. CGDisplayCopyAllDisplayModes() does not return upscaled display - // mode unless explicitly asked with kCGDisplayShowDuplicateLowResolutionModes + // We want to select the display mode with the same scaling factor as the + // current view to avoid cropping or scaling issues. + // CGDisplayCopyAllDisplayModes() does not return upscaled display modes + // nor the current mode, unless explicitly asked with + // kCGDisplayShowDuplicateLowResolutionModes // (which is undocumented...). - bool macos_10_15_or_higher = false; - if (@available(macOS 10.15, *)) { - const CFStringRef dictkeys[] = {kCGDisplayShowDuplicateLowResolutionModes}; - const CFBooleanRef dictvalues[] = {kCFBooleanTrue}; - options = CFDictionaryCreate(NULL, - (const void **)dictkeys, - (const void **)dictvalues, - 1, - &kCFCopyStringDictionaryKeyCallBacks, - &kCFTypeDictionaryValueCallBacks); - macos_10_15_or_higher = true; - } + const CFStringRef dictkeys[] = {kCGDisplayShowDuplicateLowResolutionModes}; + const CFBooleanRef dictvalues[] = {kCFBooleanTrue}; + options = CFDictionaryCreate(NULL, + (const void **)dictkeys, + (const void **)dictvalues, + 1, + &kCFCopyStringDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks); CFMutableArrayRef valid_modes; valid_modes = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks); @@ -985,29 +1039,39 @@ find_display_modes(int width, int height) { size_t num_modes = CFArrayGetCount(modes); CGDisplayModeRef mode; - - // Get the current refresh rate and pixel encoding. - CFStringRef current_pixel_encoding; - double refresh_rate; mode = CGDisplayCopyDisplayMode(_display); + // Calculate requested display size and pixel size + CGSize display_size; + CGSize pixel_size; + if (dpi_aware) { + pixel_size = NSMakeSize(width, height); + display_size = [_view convertSizeFromBacking:pixel_size]; + } else { + display_size = NSMakeSize(width, height); + // Calculate the pixel width and height of the fullscreen mode we want using + // the current display mode dimensions and pixel dimensions. + size_t pixel_width = (size_t(width) * CGDisplayModeGetPixelWidth(mode)) / CGDisplayModeGetWidth(mode); + size_t pixel_height = (size_t(height) * CGDisplayModeGetPixelHeight(mode)) / CGDisplayModeGetHeight(mode); + pixel_size = NSMakeSize(pixel_width, pixel_height); + } + // First check if the current mode is adequate. - // This test not done for macOS 10.15 and above as the mode resolution is - // not enough to identify a mode. - if (!macos_10_15_or_higher && - CGDisplayModeGetWidth(mode) == width && - CGDisplayModeGetHeight(mode) == height) { + if (CGDisplayModeGetWidth(mode) == display_size.width && + CGDisplayModeGetHeight(mode) == display_size.height && + CGDisplayModeGetPixelWidth(mode) == pixel_size.width && + CGDisplayModeGetPixelHeight(mode) == pixel_size.height) { CFArrayAppendValue(valid_modes, mode); CGDisplayModeRelease(mode); return valid_modes; } + // Get the current refresh rate and pixel encoding. + CFStringRef current_pixel_encoding; + double refresh_rate; + current_pixel_encoding = CGDisplayModeCopyPixelEncoding(mode); refresh_rate = CGDisplayModeGetRefreshRate(mode); - // Calculate the pixel width and height of the fullscreen mode we want using - // the currentdisplay mode dimensions and pixel dimensions. - size_t expected_pixel_width = (size_t(width) * CGDisplayModeGetPixelWidth(mode)) / CGDisplayModeGetWidth(mode); - size_t expected_pixel_height = (size_t(height) * CGDisplayModeGetPixelHeight(mode)) / CGDisplayModeGetHeight(mode); CGDisplayModeRelease(mode); for (size_t i = 0; i < num_modes; ++i) { @@ -1015,17 +1079,15 @@ find_display_modes(int width, int height) { CFStringRef pixel_encoding = CGDisplayModeCopyPixelEncoding(mode); - // As explained above, we want to select the fullscreen display mode using - // the same scaling factor, but only for MacOS 10.15+ To do this we check - // the mode width and height but also actual pixel widh and height. - if (CGDisplayModeGetWidth(mode) == width && - CGDisplayModeGetHeight(mode) == height && + // We select the fullscreen display mode using he same scaling factor + // To do this we check the mode width and height but also actual pixel widh + // and height. + if (CGDisplayModeGetWidth(mode) == display_size.width && + CGDisplayModeGetHeight(mode) == display_size.height && (int)(CGDisplayModeGetRefreshRate(mode) + 0.5) == (int)(refresh_rate + 0.5) && - (!macos_10_15_or_higher || - (CGDisplayModeGetPixelWidth(mode) == expected_pixel_width && - CGDisplayModeGetPixelHeight(mode) == expected_pixel_height)) && + CGDisplayModeGetPixelWidth(mode) == pixel_size.width && + CGDisplayModeGetPixelHeight(mode) == pixel_size.height && CFStringCompare(pixel_encoding, current_pixel_encoding, 0) == kCFCompareEqualTo) { - if (CGDisplayModeGetRefreshRate(mode) == refresh_rate) { // Exact match for refresh rate, prioritize this. CFArrayInsertValueAtIndex(valid_modes, 0, mode); @@ -1103,14 +1165,25 @@ do_switch_fullscreen(CGDisplayModeRef mode) { NSRect frame = [[[_view window] screen] frame]; if (cocoadisplay_cat.is_debug()) { - NSString *str = NSStringFromRect(frame); + NSString *str = NSStringFromSize([_view convertSizeToBacking:frame.size]); cocoadisplay_cat.debug() - << "Switched to fullscreen, screen rect is now " << [str UTF8String] << "\n"; + << "Switched to fullscreen, screen size is now " << [str UTF8String] << "\n"; } if (_window != nil) { + // For some reason, setting the style mask makes it give up its + // first-responder status. + if ([_window respondsToSelector:@selector(setStyleMask:)]) { + [_window setStyleMask:NSBorderlessWindowMask]; + } + [_window makeFirstResponder:_view]; + [_window setLevel:CGShieldingWindowLevel()]; + [_window makeKeyAndOrderFront:nil]; + + // Window and view frame must be updated *after* the window reconfiguration + // or the size is not set properly ! [_window setFrame:frame display:YES]; - [_view setFrame:NSMakeRect(0, 0, frame.size.width, frame.size.height)]; + [_view setFrame:frame]; [_window update]; } } @@ -1252,19 +1325,25 @@ load_cursor(const Filename &filename) { */ void CocoaGraphicsWindow:: handle_move_event() { - // Remember, Mac OS X uses flipped coordinates NSRect frame; + NSRect container; int x, y; + // Again, we are using the view to convert the frame and container size from + // points to pixels. if (_window == nil) { - frame = [_view frame]; - x = frame.origin.x; - y = [[_view superview] bounds].size.height - frame.origin.y - frame.size.height; + frame = [_view convertRectToBacking:[_view frame]]; + container = [_view convertRectToBacking:[[_view superview] frame]]; } else { - frame = [_window contentRectForFrameRect:[_window frame]]; - x = frame.origin.x; - y = [[_window screen] frame].size.height - frame.origin.y - frame.size.height; + frame = [_view convertRectToBacking:[_window contentRectForFrameRect:[_window frame]]]; + NSScreen *screen = [_window screen]; + nassertv(screen != nil); + container = [_view convertRectToBacking:[screen frame]]; } + // Y coordinate in backing store is not flipped, but origin is still at the bottom left + x = frame.origin.x; + y = container.size.height + frame.origin.y; + if (x != _properties.get_x_origin() || y != _properties.get_y_origin()) { @@ -1290,7 +1369,7 @@ handle_resize_event() { [_view setFrameSize:contentRect.size]; } - NSRect frame = [_view convertRect:[_view bounds] toView:nil]; + NSRect frame = [_view convertRectToBacking:[_view bounds]]; WindowProperties properties; bool changed = false; @@ -1403,6 +1482,22 @@ handle_foreground_event(bool foreground) { } } + +/** + * Called by the window delegate when the properties of backing store of the + * window have changed. + */ +void CocoaGraphicsWindow:: +handle_backing_change_event() { + if (cocoadisplay_cat.is_debug()) { + cocoadisplay_cat.debug() << "Backing store properties have changed\n"; + } + // Trigger a resize event to update the window size in case the backing scale + // factor did change. + handle_resize_event(); +} + + /** * Called by the window delegate when the user requests to close the window. * This may not always be called, which is why there is also a @@ -1693,6 +1788,13 @@ void CocoaGraphicsWindow:: handle_mouse_moved_event(bool in_window, double x, double y, bool absolute) { double nx, ny; + // Mouse position is received in screen points and not pixels, but in Panda3D + // we want to have the coordinates expressed in pixels. + // Instead of using convertPointFrom/toBackingStore and have complex logic to + // cope with the change of coordinate system, we cheat and directly use the + // contents scale of the view layer to convert screen point into pixels and + // vice-versa. + CGFloat contents_scale = _view.layer.contentsScale; if (absolute) { if (cocoadisplay_cat.is_spam()) { if (in_window != _input->get_pointer().get_in_window()) { @@ -1704,14 +1806,14 @@ handle_mouse_moved_event(bool in_window, double x, double y, bool absolute) { } } - nx = x; - ny = y; + nx = x * contents_scale; + ny = y * contents_scale; } else { // We received deltas, so add it to the current mouse position. PointerData md = _input->get_pointer(); - nx = md.get_x() + x; - ny = md.get_y() + y; + nx = md.get_x() + x * contents_scale; + ny = md.get_y() + y * contents_scale; } if (_properties.get_mouse_mode() == WindowProperties::M_confined @@ -1721,11 +1823,13 @@ handle_mouse_moved_event(bool in_window, double x, double y, bool absolute) { nx = std::max(0., std::min((double) get_x_size() - 1, nx)); ny = std::max(0., std::min((double) get_y_size() - 1, ny)); + // Convert back mouse position to screen space using point units if (_properties.get_fullscreen()) { - point = CGPointMake(nx, ny); + point = CGPointMake(nx / contents_scale, + ny / contents_scale); } else { - point = CGPointMake(nx + _properties.get_x_origin(), - ny + _properties.get_y_origin()); + point = CGPointMake((nx + _properties.get_x_origin()) / contents_scale, + (ny + _properties.get_y_origin()) / contents_scale); } if (CGWarpMouseCursorPosition(point) == kCGErrorSuccess) { diff --git a/panda/src/cocoadisplay/cocoaPandaWindowDelegate.h b/panda/src/cocoadisplay/cocoaPandaWindowDelegate.h index 490db59ee8..0ada863fd6 100644 --- a/panda/src/cocoadisplay/cocoaPandaWindowDelegate.h +++ b/panda/src/cocoadisplay/cocoaPandaWindowDelegate.h @@ -29,6 +29,7 @@ class CocoaGraphicsWindow; - (void)windowDidDeminiaturize:(NSNotification *)notification; - (void)windowDidBecomeKey:(NSNotification *)notification; - (void)windowDidResignKey:(NSNotification *)notification; +- (void)windowDidChangeBackingProperties:(NSNotification *)notification; - (BOOL)windowShouldClose:(id)sender; - (void)windowWillClose:(id)sender; diff --git a/panda/src/cocoadisplay/cocoaPandaWindowDelegate.mm b/panda/src/cocoadisplay/cocoaPandaWindowDelegate.mm index fe67c50c13..b5817f977a 100644 --- a/panda/src/cocoadisplay/cocoaPandaWindowDelegate.mm +++ b/panda/src/cocoadisplay/cocoaPandaWindowDelegate.mm @@ -51,6 +51,10 @@ _graphicsWindow->handle_foreground_event(false); } +- (void) windowDidChangeBackingProperties:(NSNotification *)notification { + _graphicsWindow->handle_backing_change_event(); +} + - (BOOL) windowShouldClose:(id)sender { if (cocoadisplay_cat.is_debug()) { cocoadisplay_cat.debug() diff --git a/panda/src/cocoadisplay/config_cocoadisplay.h b/panda/src/cocoadisplay/config_cocoadisplay.h index 43051a67c5..fff6c5ba67 100644 --- a/panda/src/cocoadisplay/config_cocoadisplay.h +++ b/panda/src/cocoadisplay/config_cocoadisplay.h @@ -21,6 +21,7 @@ NotifyCategoryDecl(cocoadisplay, EXPCL_PANDA_COCOADISPLAY, EXPTP_PANDA_COCOADISPLAY); extern ConfigVariableBool cocoa_invert_wheel_x; +extern ConfigVariableBool dpi_aware; extern EXPCL_PANDA_COCOADISPLAY void init_libcocoadisplay(); diff --git a/panda/src/cocoadisplay/config_cocoadisplay.mm b/panda/src/cocoadisplay/config_cocoadisplay.mm index 17688586c2..5877f88dd2 100644 --- a/panda/src/cocoadisplay/config_cocoadisplay.mm +++ b/panda/src/cocoadisplay/config_cocoadisplay.mm @@ -32,6 +32,11 @@ ConfigVariableBool cocoa_invert_wheel_x ("cocoa-invert-wheel-x", false, PRC_DESC("Set this to true to swap the wheel_left and wheel_right mouse " "button events, to restore to the pre-1.10.12 behavior.")); +ConfigVariableBool dpi_aware +("dpi-aware", false, + PRC_DESC("The default behavior on macOS is for Panda3D to use upscaling on" + "high DPI screen. Set this to true to let the application use the" + "actual pixel density of the screen.")); /** * Initializes the library. This must be called at least once before any of From b6eee1045a4c7469f1e75b22ef0e4ecb9df76394 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 11 Oct 2023 00:08:25 +0200 Subject: [PATCH 12/29] direct: Reduce dependence on builtins, satisfy mypy a bit more --- direct/src/directtools/DirectManipulation.py | 420 ++++++++++--------- direct/src/directtools/DirectSession.py | 98 +++-- direct/src/leveleditor/LevelEditorUIBase.py | 55 +-- direct/src/leveleditor/LevelLoader.py | 3 + direct/src/showbase/ShowBase.py | 6 +- direct/src/showbase/ShowBaseGlobal.py | 2 + direct/src/showutil/TexMemWatcher.py | 10 +- direct/src/tkpanels/DirectSessionPanel.py | 179 ++++---- direct/src/tkpanels/Inspector.py | 5 +- direct/src/tkpanels/Placer.py | 51 +-- direct/src/wxwidgets/WxPandaShell.py | 82 ++-- 11 files changed, 488 insertions(+), 423 deletions(-) diff --git a/direct/src/directtools/DirectManipulation.py b/direct/src/directtools/DirectManipulation.py index d77a7de7e3..98d48a9aa5 100644 --- a/direct/src/directtools/DirectManipulation.py +++ b/direct/src/directtools/DirectManipulation.py @@ -14,6 +14,8 @@ from panda3d.core import ( ) from direct.showbase.DirectObject import DirectObject from direct.showbase.MessengerGlobal import messenger +from direct.showbase.ShowBaseGlobal import hidden +from direct.showbase import ShowBaseGlobal from . import DirectGlobals as DG from .DirectUtil import useDirectRenderStyle from .DirectGeometry import ( @@ -28,6 +30,7 @@ from .DirectSelection import SelectionRay from direct.task import Task from direct.task.TaskManagerGlobal import taskMgr from copy import deepcopy +from typing import Optional class DirectManipulationControl(DirectObject): @@ -37,14 +40,16 @@ class DirectManipulationControl(DirectObject): self.hitPt = Point3(0) self.prevHit = Vec3(0) + self.widgetList: list[ObjectHandles] = [] + self.hitPtScale = Point3(0) # [gjeon] to be used in new LE's camera control self.prevHitScale = Vec3(0) # [gjeon] to be used in new LE's camera control self.rotationCenter = Point3(0) self.initScaleMag = 1 - self.manipRef = base.direct.group.attachNewNode('manipRef') + self.manipRef = ShowBaseGlobal.direct.group.attachNewNode('manipRef') self.hitPtDist = 0 - self.constraint = None + self.constraint: Optional[str] = None self.rotateAxis = 'x' self.lastCrankAngle = 0 self.fSetCoa = 0 @@ -91,8 +96,8 @@ class DirectManipulationControl(DirectObject): self.fGridSnap = 0 def scaleWidget(self, factor): - if hasattr(base.direct, 'widget'): - base.direct.widget.multiplyScalingFactorBy(factor) + if hasattr(ShowBaseGlobal.direct, 'widget'): + ShowBaseGlobal.direct.widget.multiplyScalingFactorBy(factor) else: self.objectHandles.multiplyScalingFactorBy(factor) @@ -127,7 +132,7 @@ class DirectManipulationControl(DirectObject): # Start out in select mode self.mode = 'select' - if base.direct.cameraControl.useMayaCamControls and modifiers == 4: + if ShowBaseGlobal.direct.cameraControl.useMayaCamControls and modifiers == 4: self.mode = 'camera' if self.fAllowSelectionOnly: @@ -137,7 +142,7 @@ class DirectManipulationControl(DirectObject): self.fScaling3D == 0: # Check for a widget hit point - entry = base.direct.iRay.pickWidget(skipFlags = DG.SKIP_WIDGET) + entry = ShowBaseGlobal.direct.iRay.pickWidget(skipFlags = DG.SKIP_WIDGET) # Did we hit a widget? if entry: # Yes! @@ -149,13 +154,13 @@ class DirectManipulationControl(DirectObject): # Nope, off the widget, no constraint self.constraint = None # [gjeon] to prohibit unwanted object movement while direct window doesn't have focus - if base.direct.cameraControl.useMayaCamControls and not base.direct.gotControl(modifiers) \ + if ShowBaseGlobal.direct.cameraControl.useMayaCamControls and not ShowBaseGlobal.direct.gotControl(modifiers) \ and not self.fAllowMarquee: return else: entry = None - if not base.direct.gotAlt(modifiers): + if not ShowBaseGlobal.direct.gotAlt(modifiers): if entry: # Check to see if we are moving the object # We are moving the object if we either wait long enough @@ -165,18 +170,18 @@ class DirectManipulationControl(DirectObject): # Or we move far enough self.moveDir = None watchMouseTask = Task.Task(self.watchMouseTask) - watchMouseTask.initX = base.direct.dr.mouseX - watchMouseTask.initY = base.direct.dr.mouseY + watchMouseTask.initX = ShowBaseGlobal.direct.dr.mouseX + watchMouseTask.initY = ShowBaseGlobal.direct.dr.mouseY taskMgr.add(watchMouseTask, 'manip-watch-mouse') else: - if base.direct.fControl: + if ShowBaseGlobal.direct.fControl: self.mode = 'move' self.manipulateObject() - elif not base.direct.fAlt and self.fAllowMarquee: + elif not ShowBaseGlobal.direct.fAlt and self.fAllowMarquee: self.moveDir = None watchMarqueeTask = Task.Task(self.watchMarqueeTask) - watchMarqueeTask.initX = base.direct.dr.mouseX - watchMarqueeTask.initY = base.direct.dr.mouseY + watchMarqueeTask.initX = ShowBaseGlobal.direct.dr.mouseX + watchMarqueeTask.initY = ShowBaseGlobal.direct.dr.mouseY taskMgr.add(watchMarqueeTask, 'manip-marquee-mouse') def switchToWorldSpaceMode(self): @@ -192,8 +197,8 @@ class DirectManipulationControl(DirectObject): return Task.done def watchMouseTask(self, state): - if (abs(state.initX - base.direct.dr.mouseX) > 0.01 or - abs(state.initY - base.direct.dr.mouseY) > 0.01): + if (abs(state.initX - ShowBaseGlobal.direct.dr.mouseX) > 0.01 or + abs(state.initY - ShowBaseGlobal.direct.dr.mouseY) > 0.01): taskMgr.remove('manip-move-wait') self.mode = 'move' self.manipulateObject() @@ -213,19 +218,19 @@ class DirectManipulationControl(DirectObject): self.marquee.removeNode() self.marquee = None - if base.direct.cameraControl.useMayaCamControls and base.direct.fAlt: + if ShowBaseGlobal.direct.cameraControl.useMayaCamControls and ShowBaseGlobal.direct.fAlt: return - if base.direct.fControl: + if ShowBaseGlobal.direct.fControl: return - endX = base.direct.dr.mouseX - endY = base.direct.dr.mouseY + endX = ShowBaseGlobal.direct.dr.mouseX + endY = ShowBaseGlobal.direct.dr.mouseY if (abs(endX - startX) < 0.01 and abs(endY - startY) < 0.01): return - self.marquee = LineNodePath(base.render2d, 'marquee', 0.5, VBase4(.8, .6, .6, 1)) + self.marquee = LineNodePath(ShowBaseGlobal.base.render2d, 'marquee', 0.5, VBase4(.8, .6, .6, 1)) self.marqueeInfo = (startX, startY, endX, endY) self.marquee.drawLines([ [(startX, 0, startY), (startX, 0, endY)], @@ -235,15 +240,17 @@ class DirectManipulationControl(DirectObject): self.marquee.create() if self.fMultiView: - DG.LE_showInOneCam(self.marquee, base.direct.camera.getName()) + DG.LE_showInOneCam(self.marquee, ShowBaseGlobal.direct.camera.getName()) def manipulationStop(self): taskMgr.remove('manipulateObject') taskMgr.remove('manip-move-wait') taskMgr.remove('manip-watch-mouse') taskMgr.remove('manip-marquee-mouse') + direct = ShowBaseGlobal.direct # depending on flag..... if self.mode == 'select': + base = ShowBaseGlobal.base # Check for object under mouse # Don't intersect with hidden or backfacing objects, as well as any # optionally specified things @@ -254,7 +261,7 @@ class DirectManipulationControl(DirectObject): if self.marquee: self.marquee.removeNode() self.marquee = None - base.direct.deselectAll() + direct.deselectAll() startX = self.marqueeInfo[0] startY = self.marqueeInfo[1] @@ -270,31 +277,31 @@ class DirectManipulationControl(DirectObject): nur = Point3(0, 0, 0) nul = Point3(0, 0, 0) - lens = base.direct.cam.node().getLens() + lens = direct.cam.node().getLens() lens.extrude((startX, startY), nul, ful) lens.extrude((endX, startY), nur, fur) lens.extrude((endX, endY), nlr, flr) lens.extrude((startX, endY), nll, fll) marqueeFrustum = BoundingHexahedron(fll, flr, fur, ful, nll, nlr, nur, nul) - marqueeFrustum.xform(base.direct.cam.getNetTransform().getMat()) + marqueeFrustum.xform(direct.cam.getNetTransform().getMat()) base.marqueeFrustum = marqueeFrustum def findTaggedNodePath(nodePath): # Select tagged object if present - for tag in base.direct.selected.tagList: + for tag in direct.selected.tagList: if nodePath.hasNetTag(tag): nodePath = nodePath.findNetTag(tag) return nodePath return None selectionList = [] - for geom in render.findAllMatches("**/+GeomNode"): + for geom in base.render.findAllMatches("**/+GeomNode"): if (skipFlags & DG.SKIP_HIDDEN) and geom.isHidden(): # Skip if hidden node continue -## elif (skipFlags & DG.SKIP_BACKFACE) and base.direct.iRay.isEntryBackfacing(): +## elif (skipFlags & DG.SKIP_BACKFACE) and direct.iRay.isEntryBackfacing(): ## # Skip, if backfacing poly ## pass elif (skipFlags & DG.SKIP_CAMERA) and \ @@ -303,7 +310,7 @@ class DirectManipulationControl(DirectObject): continue # Can pick unpickable, use the first visible node elif (skipFlags & DG.SKIP_UNPICKABLE) and \ - (geom.getName() in base.direct.iRay.unpickable): + (geom.getName() in direct.iRay.unpickable): # Skip if in unpickable list continue @@ -350,84 +357,86 @@ class DirectManipulationControl(DirectObject): selectionList.append(nodePath) for nodePath in selectionList: - base.direct.select(nodePath, 1) + direct.select(nodePath, 1) else: - entry = base.direct.iRay.pickGeom(skipFlags = skipFlags) + entry = direct.iRay.pickGeom(skipFlags = skipFlags) if entry: # Record hit point information self.hitPt.assign(entry.getSurfacePoint(entry.getFromNodePath())) self.hitPtDist = Vec3(self.hitPt).length() # Select it - base.direct.select(entry.getIntoNodePath(), base.direct.fShift) + direct.select(entry.getIntoNodePath(), direct.fShift) else: - base.direct.deselectAll() + direct.deselectAll() #elif self.mode == 'move': self.manipulateObjectCleanup() self.mode = None def manipulateObjectCleanup(self): + direct = ShowBaseGlobal.direct if self.fScaling3D or self.fScaling1D: # We had been scaling, need to reset object handles - if hasattr(base.direct, 'widget'): - base.direct.widget.transferObjectHandlesScale() + if hasattr(direct, 'widget'): + direct.widget.transferObjectHandlesScale() else: self.objectHandles.transferObjectHandlesScale() self.fScaling3D = 0 self.fScaling1D = 0 - base.direct.selected.highlightAll() - if hasattr(base.direct, 'widget'): - base.direct.widget.showAllHandles() + direct.selected.highlightAll() + if hasattr(direct, 'widget'): + direct.widget.showAllHandles() else: self.objectHandles.showAllHandles() - if base.direct.clusterMode == 'client': - cluster( - 'base.direct.manipulationControl.objectHandles.showAllHandles()') - if hasattr(base.direct, 'widget'): - base.direct.widget.hideGuides() + if direct.clusterMode == 'client': + direct.cluster( + 'direct.manipulationControl.objectHandles.showAllHandles()') + if hasattr(direct, 'widget'): + direct.widget.hideGuides() else: self.objectHandles.hideGuides() # Restart followSelectedNodePath task self.spawnFollowSelectedNodePathTask() messenger.send('DIRECT_manipulateObjectCleanup', - [base.direct.selected.getSelectedAsList()]) + [direct.selected.getSelectedAsList()]) def spawnFollowSelectedNodePathTask(self): # If nothing selected, just return - if not base.direct.selected.last: + if not ShowBaseGlobal.direct.selected.last: return # Clear out old task to make sure taskMgr.remove('followSelectedNodePath') # Where are the object handles relative to the selected object pos = VBase3(0) hpr = VBase3(0) - decomposeMatrix(base.direct.selected.last.mCoa2Dnp, + decomposeMatrix(ShowBaseGlobal.direct.selected.last.mCoa2Dnp, VBase3(0), hpr, pos, CSDefault) # Create the task t = Task.Task(self.followSelectedNodePathTask) # Update state variables t.pos = pos t.hpr = hpr - t.base = base.direct.selected.last + t.base = ShowBaseGlobal.direct.selected.last # Spawn the task taskMgr.add(t, 'followSelectedNodePath') def followSelectedNodePathTask(self, state): - if hasattr(base.direct, "manipulationControl") and base.direct.manipulationControl.fMultiView: - for widget in base.direct.manipulationControl.widgetList: + if hasattr(ShowBaseGlobal.direct, "manipulationControl") and ShowBaseGlobal.direct.manipulationControl.fMultiView: + for widget in ShowBaseGlobal.direct.manipulationControl.widgetList: if self.worldSpaceManip: widget.setPos(state.base, state.pos) - widget.setHpr(render, VBase3(0)) + widget.setHpr(ShowBaseGlobal.base.render, VBase3(0)) else: widget.setPosHpr(state.base, state.pos, state.hpr) else: + widget = ShowBaseGlobal.direct.widget if self.worldSpaceManip: widget.setPos(state.base, state.pos) - widget.setHpr(render, VBase3(0)) + widget.setHpr(ShowBaseGlobal.base.render, VBase3(0)) else: - base.direct.widget.setPosHpr(state.base, state.pos, state.hpr) + widget.setPosHpr(state.base, state.pos, state.hpr) return Task.cont def enableManipulation(self): @@ -459,18 +468,18 @@ class DirectManipulationControl(DirectObject): self.fSetCoa = 1 - self.fSetCoa if self.fSetCoa: - if hasattr(base.direct, 'widget'): - base.direct.widget.coaModeColor() + if hasattr(ShowBaseGlobal.direct, 'widget'): + ShowBaseGlobal.direct.widget.coaModeColor() else: self.objectHandles.coaModeColor() else: - if hasattr(base.direct, 'widget'): - base.direct.widget.manipModeColor() + if hasattr(ShowBaseGlobal.direct, 'widget'): + ShowBaseGlobal.direct.widget.manipModeColor() else: self.objectHandles.manipModeColor() else: - if hasattr(base.direct, 'widget'): - base.direct.widget.disabledModeColor() + if hasattr(ShowBaseGlobal.direct, 'widget'): + ShowBaseGlobal.direct.widget.disabledModeColor() else: self.objectHandles.disabledModeColor() @@ -480,20 +489,20 @@ class DirectManipulationControl(DirectObject): def enableWidgetMove(self): self.fMovable = 1 if self.fSetCoa: - if hasattr(base.direct, 'widget'): - base.direct.widget.coaModeColor() + if hasattr(ShowBaseGlobal.direct, 'widget'): + ShowBaseGlobal.direct.widget.coaModeColor() else: self.objectHandles.coaModeColor() else: - if hasattr(base.direct, 'widget'): - base.direct.widget.manipModeColor() + if hasattr(ShowBaseGlobal.direct, 'widget'): + ShowBaseGlobal.direct.widget.manipModeColor() else: self.objectHandles.manipModeColor() def disableWidgetMove(self): self.fMovable = 0 - if hasattr(base.direct, 'widget'): - base.direct.widget.disabledModeColor() + if hasattr(ShowBaseGlobal.direct, 'widget'): + ShowBaseGlobal.direct.widget.disabledModeColor() else: self.objectHandles.disabledModeColor() @@ -519,7 +528,8 @@ class DirectManipulationControl(DirectObject): def manipulateObject(self): # Only do this if something is selected - selectedList = base.direct.selected.getSelectedAsList() + direct = ShowBaseGlobal.direct + selectedList = direct.selected.getSelectedAsList() # See if any of the selected are completely uneditable editTypes = self.getEditTypes(selectedList) if (editTypes & DG.EDIT_TYPE_UNEDITABLE) == DG.EDIT_TYPE_UNEDITABLE: @@ -533,25 +543,26 @@ class DirectManipulationControl(DirectObject): # Set manipulation flag self.fManip = 1 # Record undo point - base.direct.pushUndo(base.direct.selected) + direct.pushUndo(direct.selected) # Update object handles visibility - if hasattr(base.direct, 'widget'): - base.direct.widget.showGuides() - base.direct.widget.hideAllHandles() - base.direct.widget.showHandle(self.constraint) + if hasattr(direct, 'widget'): + direct.widget.showGuides() + direct.widget.hideAllHandles() + direct.widget.showHandle(self.constraint) else: self.objectHandles.showGuides() self.objectHandles.hideAllHandles() self.objectHandles.showHandle(self.constraint) - if base.direct.clusterMode == 'client': - oh = 'base.direct.manipulationControl.objectHandles' + if direct.clusterMode == 'client': + oh = 'direct.manipulationControl.objectHandles' + cluster = direct.cluster cluster(oh + '.showGuides()', 0) cluster(oh + '.hideAllHandles()', 0) cluster(oh + ('.showHandle("%s")'% self.constraint), 0) # Record relationship between selected nodes and widget - base.direct.selected.getWrtAll() + direct.selected.getWrtAll() # hide the bbox of the selected objects during interaction - base.direct.selected.dehighlightAll() + direct.selected.dehighlightAll() # Send event to signal start of manipulation messenger.send('DIRECT_manipulateObjectStart') # Manipulate the real object with the constraint @@ -567,14 +578,14 @@ class DirectManipulationControl(DirectObject): self.fScaleInit1 = 1 # record initial offset between widget and camera t = Task.Task(self.manipulateObjectTask) - t.fMouseX = abs(base.direct.dr.mouseX) > 0.9 - t.fMouseY = abs(base.direct.dr.mouseY) > 0.9 + t.fMouseX = abs(ShowBaseGlobal.direct.dr.mouseX) > 0.9 + t.fMouseY = abs(ShowBaseGlobal.direct.dr.mouseY) > 0.9 if t.fMouseX: t.constrainedDir = 'y' else: t.constrainedDir = 'x' # Compute widget's xy coords in screen space - t.coaCenter = getScreenXY(base.direct.widget) + t.coaCenter = getScreenXY(ShowBaseGlobal.direct.widget) # These are used to rotate about view vector if t.fMouseX and t.fMouseY: t.lastAngle = getCrankAngle(t.coaCenter) @@ -597,14 +608,14 @@ class DirectManipulationControl(DirectObject): elif type == 'ring' and not self.currEditTypes & DG.EDIT_TYPE_UNROTATABLE: self.rotate1D(state) elif type == 'scale' and not self.currEditTypes & DG.EDIT_TYPE_UNSCALABLE: - if base.direct.fShift: + if ShowBaseGlobal.direct.fShift: self.fScaling3D = 1 self.scale3D(state) else: self.fScaling1D = 1 self.scale1D(state) else: - if base.direct.fControl and not self.currEditTypes & DG.EDIT_TYPE_UNSCALABLE: + if ShowBaseGlobal.direct.fControl and not self.currEditTypes & DG.EDIT_TYPE_UNSCALABLE: if type == 'post': # [gjeon] non-uniform scaling self.fScaling1D = 1 @@ -623,16 +634,16 @@ class DirectManipulationControl(DirectObject): # No widget interaction, determine free manip mode elif self.fFreeManip and not self.useSeparateScaleHandles: # If we've been scaling and changed modes, reset object handles - if 0 and (self.fScaling1D or self.fScaling3D) and (not base.direct.fAlt): - if hasattr(base.direct, 'widget'): - base.direct.widget.transferObjectHandleScale() + if 0 and (self.fScaling1D or self.fScaling3D) and (not ShowBaseGlobal.direct.fAlt): + if hasattr(ShowBaseGlobal.direct, 'widget'): + ShowBaseGlobal.direct.widget.transferObjectHandleScale() else: self.objectHandles.transferObjectHandlesScale() self.fScaling1D = 0 self.fScaling3D = 0 # Alt key switches to a scaling mode - if base.direct.fControl and not self.currEditTypes & DG.EDIT_TYPE_UNSCALABLE: + if ShowBaseGlobal.direct.fControl and not self.currEditTypes & DG.EDIT_TYPE_UNSCALABLE: self.fScaling3D = 1 self.scale3D(state) # Otherwise, manip mode depends on where you started @@ -645,7 +656,7 @@ class DirectManipulationControl(DirectObject): elif not self.currEditTypes & DG.EDIT_TYPE_UNMOVABLE: # Mouse started in central region, xlate # Mode depends on shift key - if base.direct.fShift or base.direct.fControl: + if ShowBaseGlobal.direct.fShift or ShowBaseGlobal.direct.fControl: self.xlateCamXY(state) else: self.xlateCamXZ(state) @@ -653,11 +664,11 @@ class DirectManipulationControl(DirectObject): return Task.done if self.fSetCoa: # Update coa based on current widget position - base.direct.selected.last.mCoa2Dnp.assign( - base.direct.widget.getMat(base.direct.selected.last)) + ShowBaseGlobal.direct.selected.last.mCoa2Dnp.assign( + ShowBaseGlobal.direct.widget.getMat(ShowBaseGlobal.direct.selected.last)) else: # Move the objects with the widget - base.direct.selected.moveWrtWidgetAll() + ShowBaseGlobal.direct.selected.moveWrtWidgetAll() # Continue return Task.cont @@ -677,39 +688,41 @@ class DirectManipulationControl(DirectObject): signX = -1.0 else: signX = 1.0 - modX = math.fabs(offsetX) % base.direct.grid.gridSpacing - floorX = math.floor(math.fabs(offsetX) / base.direct.grid.gridSpacing) - if modX < base.direct.grid.gridSpacing / 2.0: - offsetX = signX * floorX * base.direct.grid.gridSpacing + modX = math.fabs(offsetX) % ShowBaseGlobal.direct.grid.gridSpacing + floorX = math.floor(math.fabs(offsetX) / ShowBaseGlobal.direct.grid.gridSpacing) + if modX < ShowBaseGlobal.direct.grid.gridSpacing / 2.0: + offsetX = signX * floorX * ShowBaseGlobal.direct.grid.gridSpacing else: - offsetX = signX * (floorX + 1) * base.direct.grid.gridSpacing + offsetX = signX * (floorX + 1) * ShowBaseGlobal.direct.grid.gridSpacing if offsetY < 0.0: signY = -1.0 else: signY = 1.0 - modY = math.fabs(offsetY) % base.direct.grid.gridSpacing - floorY = math.floor(math.fabs(offsetY) / base.direct.grid.gridSpacing) - if modY < base.direct.grid.gridSpacing / 2.0: - offsetY = signY * floorY * base.direct.grid.gridSpacing + modY = math.fabs(offsetY) % ShowBaseGlobal.direct.grid.gridSpacing + floorY = math.floor(math.fabs(offsetY) / ShowBaseGlobal.direct.grid.gridSpacing) + if modY < ShowBaseGlobal.direct.grid.gridSpacing / 2.0: + offsetY = signY * floorY * ShowBaseGlobal.direct.grid.gridSpacing else: - offsetY = signY * (floorY + 1) * base.direct.grid.gridSpacing + offsetY = signY * (floorY + 1) * ShowBaseGlobal.direct.grid.gridSpacing if offsetZ < 0.0: signZ = -1.0 else: signZ = 1.0 - modZ = math.fabs(offsetZ) % base.direct.grid.gridSpacing - floorZ = math.floor(math.fabs(offsetZ) / base.direct.grid.gridSpacing) - if modZ < base.direct.grid.gridSpacing / 2.0: - offsetZ = signZ * floorZ * base.direct.grid.gridSpacing + modZ = math.fabs(offsetZ) % ShowBaseGlobal.direct.grid.gridSpacing + floorZ = math.floor(math.fabs(offsetZ) / ShowBaseGlobal.direct.grid.gridSpacing) + if modZ < ShowBaseGlobal.direct.grid.gridSpacing / 2.0: + offsetZ = signZ * floorZ * ShowBaseGlobal.direct.grid.gridSpacing else: - offsetZ = signZ * (floorZ + 1) * base.direct.grid.gridSpacing + offsetZ = signZ * (floorZ + 1) * ShowBaseGlobal.direct.grid.gridSpacing return Point3(offsetX, offsetY, offsetZ) ### WIDGET MANIPULATION METHODS ### def xlate1D(self, state): + assert self.constraint is not None + # Constrained 1D Translation along widget axis # Compute nearest hit point along axis and try to keep # that point as close to the current mouse position as possible @@ -725,27 +738,29 @@ class DirectManipulationControl(DirectObject): # Move widget to keep hit point as close to mouse as possible offset = self.hitPt - self.prevHit - if hasattr(base.direct, "manipulationControl") and base.direct.manipulationControl.fMultiView: - for widget in base.direct.manipulationControl.widgetList: + if hasattr(ShowBaseGlobal.direct, "manipulationControl") and ShowBaseGlobal.direct.manipulationControl.fMultiView: + for widget in ShowBaseGlobal.direct.manipulationControl.widgetList: if self.fGridSnap: widget.setPos(self.gridSnapping(widget, offset)) else: widget.setPos(widget, offset) - #if base.direct.camera.getName() != 'persp': + #if ShowBaseGlobal.direct.camera.getName() != 'persp': #self.prevHit.assign(self.hitPt) else: if self.fGridSnap: - base.direct.widget.setPos(self.gridSnapping(base.direct.widget, offset)) + ShowBaseGlobal.direct.widget.setPos(self.gridSnapping(ShowBaseGlobal.direct.widget, offset)) else: - base.direct.widget.setPos(base.direct.widget, offset) + ShowBaseGlobal.direct.widget.setPos(ShowBaseGlobal.direct.widget, offset) def xlate2D(self, state): + assert self.constraint is not None + # Constrained 2D (planar) translation # Compute point of intersection of ray from eyepoint through cursor # to one of the three orthogonal planes on the widget. # This point tracks all subsequent mouse movements self.hitPt.assign(self.objectHandles.getWidgetIntersectPt( - base.direct.widget, self.constraint[:1])) + ShowBaseGlobal.direct.widget, self.constraint[:1])) # use it to see how far to move the widget if self.fHitInit: @@ -755,21 +770,23 @@ class DirectManipulationControl(DirectObject): else: offset = self.hitPt - self.prevHit - if hasattr(base.direct, "manipulationControl") and base.direct.manipulationControl.fMultiView: - for widget in base.direct.manipulationControl.widgetList: + if hasattr(ShowBaseGlobal.direct, "manipulationControl") and ShowBaseGlobal.direct.manipulationControl.fMultiView: + for widget in ShowBaseGlobal.direct.manipulationControl.widgetList: if self.fGridSnap: widget.setPos(self.gridSnapping(widget, offset)) else: widget.setPos(widget, offset) - if base.direct.camera.getName() != 'persp': + if ShowBaseGlobal.direct.camera.getName() != 'persp': self.prevHit.assign(self.hitPt) else: if self.fGridSnap: - base.direct.widget.setPos(self.gridSnapping(base.direct.widget, offset)) + ShowBaseGlobal.direct.widget.setPos(self.gridSnapping(ShowBaseGlobal.direct.widget, offset)) else: - base.direct.widget.setPos(base.direct.widget, offset) + ShowBaseGlobal.direct.widget.setPos(ShowBaseGlobal.direct.widget, offset) def rotate1D(self, state): + assert self.constraint is not None + # Constrained 1D rotation about the widget's main axis (X, Y, or Z) # Rotation depends upon circular motion of the mouse about the # projection of the widget's origin on the image plane @@ -781,7 +798,7 @@ class DirectManipulationControl(DirectObject): self.fHitInit = 0 self.rotateAxis = self.constraint[:1] self.fWidgetTop = self.widgetCheck('top?') - self.rotationCenter = getScreenXY(base.direct.widget) + self.rotationCenter = getScreenXY(ShowBaseGlobal.direct.widget) self.lastCrankAngle = getCrankAngle(self.rotationCenter) # Rotate widget based on how far cursor has swung around origin @@ -790,27 +807,29 @@ class DirectManipulationControl(DirectObject): if self.fWidgetTop: deltaAngle = -1 * deltaAngle if self.rotateAxis == 'x': - if hasattr(base.direct, "manipulationControl") and base.direct.manipulationControl.fMultiView: - for widget in base.direct.manipulationControl.widgetList: + if hasattr(ShowBaseGlobal.direct, "manipulationControl") and ShowBaseGlobal.direct.manipulationControl.fMultiView: + for widget in ShowBaseGlobal.direct.manipulationControl.widgetList: widget.setP(widget, deltaAngle) else: - base.direct.widget.setP(base.direct.widget, deltaAngle) + ShowBaseGlobal.direct.widget.setP(ShowBaseGlobal.direct.widget, deltaAngle) elif self.rotateAxis == 'y': - if hasattr(base.direct, "manipulationControl") and base.direct.manipulationControl.fMultiView: - for widget in base.direct.manipulationControl.widgetList: + if hasattr(ShowBaseGlobal.direct, "manipulationControl") and ShowBaseGlobal.direct.manipulationControl.fMultiView: + for widget in ShowBaseGlobal.direct.manipulationControl.widgetList: widget.setR(widget, deltaAngle) else: - base.direct.widget.setR(base.direct.widget, deltaAngle) + ShowBaseGlobal.direct.widget.setR(ShowBaseGlobal.direct.widget, deltaAngle) elif self.rotateAxis == 'z': - if hasattr(base.direct, "manipulationControl") and base.direct.manipulationControl.fMultiView: - for widget in base.direct.manipulationControl.widgetList: + if hasattr(ShowBaseGlobal.direct, "manipulationControl") and ShowBaseGlobal.direct.manipulationControl.fMultiView: + for widget in ShowBaseGlobal.direct.manipulationControl.widgetList: widget.setH(widget, deltaAngle) else: - base.direct.widget.setH(base.direct.widget, deltaAngle) + ShowBaseGlobal.direct.widget.setH(ShowBaseGlobal.direct.widget, deltaAngle) # Record crank angle for next time around self.lastCrankAngle = newAngle def widgetCheck(self, type): + assert self.constraint is not None + # Utility to see if we are looking at the top or bottom of # a 2D planar widget or if we are looking at a 2D planar widget # edge on @@ -818,7 +837,7 @@ class DirectManipulationControl(DirectObject): # widget's origin and one of the three principle axes axis = self.constraint[:1] # First compute vector from eye through widget origin - mWidget2Cam = base.direct.widget.getMat(base.direct.camera) + mWidget2Cam = ShowBaseGlobal.direct.widget.getMat(ShowBaseGlobal.direct.camera) # And determine where the viewpoint is relative to widget pos = VBase3(0) decomposeMatrix(mWidget2Cam, VBase3(0), VBase3(0), pos, @@ -850,19 +869,19 @@ class DirectManipulationControl(DirectObject): # Reset scaling init flag self.fScaleInit = 1 # Where is the widget relative to current camera view - vWidget2Camera = base.direct.widget.getPos(base.direct.camera) + vWidget2Camera = ShowBaseGlobal.direct.widget.getPos(ShowBaseGlobal.direct.camera) x = vWidget2Camera[0] y = vWidget2Camera[1] z = vWidget2Camera[2] # Move widget (and objects) based upon mouse motion # Scaled up accordingly based upon widget distance - dr = base.direct.dr + dr = ShowBaseGlobal.direct.dr - base.direct.widget.setX( - base.direct.camera, + ShowBaseGlobal.direct.widget.setX( + ShowBaseGlobal.direct.camera, x + 0.5 * dr.mouseDeltaX * dr.nearWidth * (y/dr.near)) - base.direct.widget.setZ( - base.direct.camera, + ShowBaseGlobal.direct.widget.setZ( + ShowBaseGlobal.direct.camera, z + 0.5 * dr.mouseDeltaY * dr.nearHeight * (y/dr.near)) def xlateCamXY(self, state): @@ -873,17 +892,17 @@ class DirectManipulationControl(DirectObject): # Reset scaling init flag self.fScaleInit = 1 # Now, where is the widget relative to current camera view - vWidget2Camera = base.direct.widget.getPos(base.direct.camera) + vWidget2Camera = ShowBaseGlobal.direct.widget.getPos(ShowBaseGlobal.direct.camera) # If this is first time around, record initial y distance if self.fHitInit: self.fHitInit = 0 # Use distance to widget to scale motion along Y self.xlateSF = Vec3(vWidget2Camera).length() # Get widget's current xy coords in screen space - coaCenter = getNearProjectionPoint(base.direct.widget) - self.deltaNearX = coaCenter[0] - base.direct.dr.nearVec[0] + coaCenter = getNearProjectionPoint(ShowBaseGlobal.direct.widget) + self.deltaNearX = coaCenter[0] - ShowBaseGlobal.direct.dr.nearVec[0] # Which way do we move the object? - if base.direct.fControl: + if ShowBaseGlobal.direct.fControl: moveDir = Vec3(vWidget2Camera) # If widget is behind camera invert vector if moveDir[1] < 0.0: @@ -892,7 +911,7 @@ class DirectManipulationControl(DirectObject): else: moveDir = Vec3(DG.Y_AXIS) # Move selected objects - dr = base.direct.dr + dr = ShowBaseGlobal.direct.dr # Scale move dir moveDir.assign(moveDir * (2.0 * dr.mouseDeltaY * self.xlateSF)) # Add it to current widget offset @@ -902,7 +921,7 @@ class DirectManipulationControl(DirectObject): (vWidget2Camera[1]/dr.near)) # Move widget - base.direct.widget.setPos(base.direct.camera, vWidget2Camera) + ShowBaseGlobal.direct.widget.setPos(ShowBaseGlobal.direct.camera, vWidget2Camera) def rotate2D(self, state): """ Virtual trackball rotation of widget """ @@ -912,17 +931,17 @@ class DirectManipulationControl(DirectObject): self.fScaleInit = 1 tumbleRate = 360 # If moving outside of center, ignore motion perpendicular to edge - if ((state.constrainedDir == 'y') and (abs(base.direct.dr.mouseX) > 0.9)): + if ((state.constrainedDir == 'y') and (abs(ShowBaseGlobal.direct.dr.mouseX) > 0.9)): deltaX = 0 - deltaY = base.direct.dr.mouseDeltaY - elif ((state.constrainedDir == 'x') and (abs(base.direct.dr.mouseY) > 0.9)): - deltaX = base.direct.dr.mouseDeltaX + deltaY = ShowBaseGlobal.direct.dr.mouseDeltaY + elif ((state.constrainedDir == 'x') and (abs(ShowBaseGlobal.direct.dr.mouseY) > 0.9)): + deltaX = ShowBaseGlobal.direct.dr.mouseDeltaX deltaY = 0 else: - deltaX = base.direct.dr.mouseDeltaX - deltaY = base.direct.dr.mouseDeltaY + deltaX = ShowBaseGlobal.direct.dr.mouseDeltaX + deltaY = ShowBaseGlobal.direct.dr.mouseDeltaY # Mouse motion edge to edge of display region results in one full turn - relHpr(base.direct.widget, base.direct.camera, deltaX * tumbleRate, + relHpr(ShowBaseGlobal.direct.widget, ShowBaseGlobal.direct.camera, deltaX * tumbleRate, -deltaY * tumbleRate, 0) def rotateAboutViewVector(self, state): @@ -935,19 +954,22 @@ class DirectManipulationControl(DirectObject): deltaAngle = angle - state.lastAngle state.lastAngle = angle # Mouse motion edge to edge of display region results in one full turn - relHpr(base.direct.widget, base.direct.camera, 0, 0, -deltaAngle) + relHpr(ShowBaseGlobal.direct.widget, ShowBaseGlobal.direct.camera, 0, 0, -deltaAngle) def scale1D(self, state): - if hasattr(base.direct, "manipulationControl") and base.direct.manipulationControl.fMultiView: + assert self.constraint is not None + + direct = ShowBaseGlobal.direct + if hasattr(direct, "manipulationControl") and direct.manipulationControl.fMultiView: self.hitPtScale.assign(self.objectHandles.getAxisIntersectPt(self.constraint[:1])) self.hitPtScale = self.objectHandles.getMat().xformVec(self.hitPtScale) if self.fScaleInit1: # First time through just record hit point self.fScaleInit1 = 0 self.prevHitScale.assign(self.hitPtScale) - self.origScale = base.direct.widget.getScale() + self.origScale = direct.widget.getScale() else: - widgetPos = base.direct.widget.getPos() + widgetPos = direct.widget.getPos() d0 = (self.prevHitScale).length() if d0 == 0: #make sure we don't divide by zero d0 = 0.001 @@ -962,7 +984,7 @@ class DirectManipulationControl(DirectObject): currScale = Vec3(currScale.getX(), currScale.getY() * d1/d0, currScale.getZ()) elif self.constraint[:1] == 'z': currScale = Vec3(currScale.getX(), currScale.getY(), currScale.getZ() * d1/d0) - base.direct.widget.setScale(currScale) + direct.widget.setScale(currScale) return # [gjeon] Constrained 1D scale of the selected node based upon up down mouse motion @@ -970,13 +992,13 @@ class DirectManipulationControl(DirectObject): self.fScaleInit = 0 self.initScaleMag = Vec3(self.objectHandles.getAxisIntersectPt(self.constraint[:1])).length() # record initial scale - self.initScale = base.direct.widget.getScale() + self.initScale = direct.widget.getScale() # Reset fHitInitFlag self.fHitInit = 1 # reset the scale of the scaling widget so the calls to # getAxisIntersectPt calculate the correct distance - base.direct.widget.setScale(1,1,1) + direct.widget.setScale(1,1,1) # Scale factor is ratio current mag with init mag if self.constraint[:1] == 'x': @@ -991,20 +1013,22 @@ class DirectManipulationControl(DirectObject): currScale = Vec3(self.initScale.getX(), self.initScale.getY(), self.initScale.getZ() * self.objectHandles.getAxisIntersectPt('z').length() / self.initScaleMag) - base.direct.widget.setScale(currScale) + direct.widget.setScale(currScale) def scale3D(self, state): - if hasattr(base.direct, "manipulationControl") and base.direct.manipulationControl.fMultiView: + direct = ShowBaseGlobal.direct + if hasattr(direct, "manipulationControl") and direct.manipulationControl.fMultiView: if self.useSeparateScaleHandles: + assert self.constraint is not None self.hitPtScale.assign(self.objectHandles.getAxisIntersectPt(self.constraint[:1])) self.hitPtScale = self.objectHandles.getMat().xformVec(self.hitPtScale) if self.fScaleInit1: # First time through just record hit point self.fScaleInit1 = 0 self.prevHitScale.assign(self.hitPtScale) - self.origScale = base.direct.widget.getScale() + self.origScale = direct.widget.getScale() else: - widgetPos = base.direct.widget.getPos() + widgetPos = direct.widget.getPos() d0 = (self.prevHitScale).length() if d0 == 0: #make sure we don't divide by zero d0 = 0.001 @@ -1014,7 +1038,7 @@ class DirectManipulationControl(DirectObject): currScale = self.origScale # Scale factor is ratio current mag with init mag currScale = Vec3(currScale.getX() * d1/d0, currScale.getY() * d1/d0, currScale.getZ() * d1/d0) - base.direct.widget.setScale(currScale) + direct.widget.setScale(currScale) return else: self.hitPtScale.assign(self.objectHandles.getMouseIntersectPt()) @@ -1023,9 +1047,9 @@ class DirectManipulationControl(DirectObject): # First time through just record hit point self.fScaleInit1 = 0 self.prevHitScale.assign(self.hitPtScale) - self.origScale = base.direct.widget.getScale() + self.origScale = direct.widget.getScale() else: - widgetPos = base.direct.widget.getPos() + widgetPos = direct.widget.getPos() d0 = (self.prevHitScale - widgetPos).length() if d0 == 0: #make sure we don't divide by zero d0 = 0.001 @@ -1034,20 +1058,20 @@ class DirectManipulationControl(DirectObject): d1 = 0.001 #make sure we don't set scale to zero currScale = self.origScale currScale = currScale * d1/d0 - base.direct.widget.setScale(currScale) + direct.widget.setScale(currScale) return # Scale the selected node based upon up down mouse motion # Mouse motion from edge to edge results in a factor of 4 scaling # From midpoint to edge doubles or halves objects scale if self.fScaleInit: self.fScaleInit = 0 - self.manipRef.setPos(base.direct.widget, 0, 0, 0) - self.manipRef.setHpr(base.direct.camera, 0, 0, 0) + self.manipRef.setPos(direct.widget, 0, 0, 0) + self.manipRef.setHpr(direct.camera, 0, 0, 0) self.initScaleMag = Vec3( self.objectHandles.getWidgetIntersectPt( self.manipRef, 'y')).length() # record initial scale - self.initScale = base.direct.widget.getScale() + self.initScale = direct.widget.getScale() # Reset fHitInitFlag self.fHitInit = 1 # Begin @@ -1058,29 +1082,29 @@ class DirectManipulationControl(DirectObject): self.manipRef, 'y').length() / self.initScaleMag) ) - base.direct.widget.setScale(currScale) + direct.widget.setScale(currScale) ## Utility functions ## def plantSelectedNodePath(self): """ Move selected object to intersection point of cursor on scene """ # Check for intersection - entry = base.direct.iRay.pickGeom( + entry = ShowBaseGlobal.direct.iRay.pickGeom( skipFlags = DG.SKIP_HIDDEN | DG.SKIP_BACKFACE | DG.SKIP_CAMERA) # MRM: Need to handle moving COA - if entry is not None and base.direct.selected.last is not None: + if entry is not None and ShowBaseGlobal.direct.selected.last is not None: # Record undo point - base.direct.pushUndo(base.direct.selected) + ShowBaseGlobal.direct.pushUndo(ShowBaseGlobal.direct.selected) # Record wrt matrix - base.direct.selected.getWrtAll() + ShowBaseGlobal.direct.selected.getWrtAll() # Move selected - base.direct.widget.setPos( - base.direct.camera, entry.getSurfacePoint(entry.getFromNodePath())) + ShowBaseGlobal.direct.widget.setPos( + ShowBaseGlobal.direct.camera, entry.getSurfacePoint(entry.getFromNodePath())) # Move all the selected objects with widget # Move the objects with the widget - base.direct.selected.moveWrtWidgetAll() + ShowBaseGlobal.direct.selected.moveWrtWidgetAll() # Let everyone know that something was moved messenger.send('DIRECT_manipulateObjectCleanup', - [base.direct.selected.getSelectedAsList()]) + [ShowBaseGlobal.direct.selected.getSelectedAsList()]) class ObjectHandles(NodePath, DirectObject): @@ -1089,7 +1113,7 @@ class ObjectHandles(NodePath, DirectObject): NodePath.__init__(self) # Load up object handles model and assign it to self - self.assign(base.loader.loadModel('models/misc/objectHandles')) + self.assign(ShowBaseGlobal.base.loader.loadModel('models/misc/objectHandles')) self.setName(name) self.scalingNode = NodePath(self) self.scalingNode.setName('ohScalingNode') @@ -1199,14 +1223,14 @@ class ObjectHandles(NodePath, DirectObject): def toggleWidget(self): if self.fActive: - if hasattr(base.direct, "manipulationControl") and base.direct.manipulationControl.fMultiView: - for widget in base.direct.manipulationControl.widgetList: + if hasattr(ShowBaseGlobal.direct, "manipulationControl") and ShowBaseGlobal.direct.manipulationControl.fMultiView: + for widget in ShowBaseGlobal.direct.manipulationControl.widgetList: widget.deactivate() else: self.deactivate() else: - if hasattr(base.direct, "manipulationControl") and base.direct.manipulationControl.fMultiView: - for widget in base.direct.manipulationControl.widgetList: + if hasattr(ShowBaseGlobal.direct, "manipulationControl") and ShowBaseGlobal.direct.manipulationControl.fMultiView: + for widget in ShowBaseGlobal.direct.manipulationControl.widgetList: widget.activate() widget.showWidgetIfActive() else: @@ -1222,10 +1246,10 @@ class ObjectHandles(NodePath, DirectObject): def showWidgetIfActive(self): if self.fActive: - self.reparentTo(base.direct.group) + self.reparentTo(ShowBaseGlobal.direct.group) def showWidget(self): - self.reparentTo(base.direct.group) + self.reparentTo(ShowBaseGlobal.direct.group) def hideWidget(self): self.reparentTo(hidden) @@ -1260,7 +1284,7 @@ class ObjectHandles(NodePath, DirectObject): self.xRingGroup.reparentTo(self.xHandles) elif handle == 'x-disc': self.xDiscGroup.reparentTo(self.xHandles) - elif handle == 'x-scale' and base.direct.manipulationControl.useSeparateScaleHandles: + elif handle == 'x-scale' and ShowBaseGlobal.direct.manipulationControl.useSeparateScaleHandles: self.xScaleGroup.reparentTo(self.xHandles) elif handle == 'y-post': self.yPostGroup.reparentTo(self.yHandles) @@ -1268,7 +1292,7 @@ class ObjectHandles(NodePath, DirectObject): self.yRingGroup.reparentTo(self.yHandles) elif handle == 'y-disc': self.yDiscGroup.reparentTo(self.yHandles) - elif handle == 'y-scale' and base.direct.manipulationControl.useSeparateScaleHandles: + elif handle == 'y-scale' and ShowBaseGlobal.direct.manipulationControl.useSeparateScaleHandles: self.yScaleGroup.reparentTo(self.yHandles) elif handle == 'z-post': self.zPostGroup.reparentTo(self.zHandles) @@ -1276,7 +1300,7 @@ class ObjectHandles(NodePath, DirectObject): self.zRingGroup.reparentTo(self.zHandles) elif handle == 'z-disc': self.zDiscGroup.reparentTo(self.zHandles) - elif handle == 'z-scale' and base.direct.manipulationControl.useSeparateScaleHandles: + elif handle == 'z-scale' and ShowBaseGlobal.direct.manipulationControl.useSeparateScaleHandles: self.zScaleGroup.reparentTo(self.zHandles) def disableHandles(self, handles): @@ -1420,9 +1444,9 @@ class ObjectHandles(NodePath, DirectObject): def growToFit(self): # Increase handles scale until they cover 30% of the min dimension - pos = base.direct.widget.getPos(base.direct.camera) - minDim = min(base.direct.dr.nearWidth, base.direct.dr.nearHeight) - sf = 0.15 * minDim * (pos[1]/base.direct.dr.near) + pos = ShowBaseGlobal.direct.widget.getPos(ShowBaseGlobal.direct.camera) + minDim = min(ShowBaseGlobal.direct.dr.nearWidth, ShowBaseGlobal.direct.dr.nearHeight) + sf = 0.15 * minDim * (pos[1]/ShowBaseGlobal.direct.dr.near) self.ohScalingFactor = sf sf = sf * self.directScalingFactor ival = self.scalingNode.scaleInterval(0.5, (sf, sf, sf), @@ -1624,14 +1648,14 @@ class ObjectHandles(NodePath, DirectObject): lines.setName('z-guide') def getAxisIntersectPt(self, axis): - if hasattr(base.direct, "manipulationControl") and base.direct.manipulationControl.fMultiView and\ - base.direct.camera.getName() != 'persp': + if hasattr(ShowBaseGlobal.direct, "manipulationControl") and ShowBaseGlobal.direct.manipulationControl.fMultiView and\ + ShowBaseGlobal.direct.camera.getName() != 'persp': # create ray from the camera to detect 3d position - iRay = SelectionRay(base.direct.camera) - iRay.collider.setFromLens(base.direct.camNode, base.direct.dr.mouseX, base.direct.dr.mouseY) + iRay = SelectionRay(ShowBaseGlobal.direct.camera) + iRay.collider.setFromLens(ShowBaseGlobal.direct.camNode, ShowBaseGlobal.direct.dr.mouseX, ShowBaseGlobal.direct.dr.mouseY) #iRay.collideWithBitMask(1) iRay.collideWithBitMask(BitMask32.bit(21)) - iRay.ct.traverse(base.direct.grid) + iRay.ct.traverse(ShowBaseGlobal.direct.grid) if iRay.getNumEntries() == 0: del iRay @@ -1653,8 +1677,8 @@ class ObjectHandles(NodePath, DirectObject): return self.hitPt # Calc the xfrom from camera to widget - mCam2Widget = base.direct.camera.getMat(base.direct.widget) - lineDir = Vec3(mCam2Widget.xformVec(base.direct.dr.nearVec)) + mCam2Widget = ShowBaseGlobal.direct.camera.getMat(ShowBaseGlobal.direct.widget) + lineDir = Vec3(mCam2Widget.xformVec(ShowBaseGlobal.direct.dr.nearVec)) lineDir.normalize() # And determine where the viewpoint is relative to widget lineOrigin = VBase3(0) @@ -1698,11 +1722,11 @@ class ObjectHandles(NodePath, DirectObject): def getMouseIntersectPt(self): # create ray from the camera to detect 3d position - iRay = SelectionRay(base.direct.camera) - iRay.collider.setFromLens(base.direct.camNode, base.direct.dr.mouseX, base.direct.dr.mouseY) + iRay = SelectionRay(ShowBaseGlobal.direct.camera) + iRay.collider.setFromLens(ShowBaseGlobal.direct.camNode, ShowBaseGlobal.direct.dr.mouseX, ShowBaseGlobal.direct.dr.mouseY) #iRay.collideWithBitMask(1) iRay.collideWithBitMask(BitMask32.bit(21)) - iRay.ct.traverse(base.direct.grid) + iRay.ct.traverse(ShowBaseGlobal.direct.grid) if iRay.getNumEntries() == 0: del iRay @@ -1713,7 +1737,7 @@ class ObjectHandles(NodePath, DirectObject): # create a temp nodePath to get the position np = NodePath('temp') - np.setPos(base.direct.camera, hitPt) + np.setPos(ShowBaseGlobal.direct.camera, hitPt) resultPt = Point3(0) resultPt.assign(np.getPos()) np.removeNode() @@ -1721,8 +1745,8 @@ class ObjectHandles(NodePath, DirectObject): return resultPt def getWidgetIntersectPt(self, nodePath, plane): - if hasattr(base.direct, "manipulationControl") and base.direct.manipulationControl.fMultiView and\ - base.direct.camera.getName() != 'persp': + if hasattr(ShowBaseGlobal.direct, "manipulationControl") and ShowBaseGlobal.direct.manipulationControl.fMultiView and\ + ShowBaseGlobal.direct.camera.getName() != 'persp': self.hitPt.assign(self.getMouseIntersectPt()) return self.hitPt @@ -1730,7 +1754,7 @@ class ObjectHandles(NodePath, DirectObject): # with the plane containing the 2D xlation or 1D rotation widgets # Calc the xfrom from camera to the nodePath - mCam2NodePath = base.direct.camera.getMat(nodePath) + mCam2NodePath = ShowBaseGlobal.direct.camera.getMat(nodePath) # And determine where the viewpoint is relative to widget lineOrigin = VBase3(0) @@ -1740,7 +1764,7 @@ class ObjectHandles(NodePath, DirectObject): # Next we find the vector from viewpoint to the widget through # the mouse's position on near plane. # This defines the intersection ray - lineDir = Vec3(mCam2NodePath.xformVec(base.direct.dr.nearVec)) + lineDir = Vec3(mCam2NodePath.xformVec(ShowBaseGlobal.direct.dr.nearVec)) lineDir.normalize() # Find the hit point if plane == 'x': diff --git a/direct/src/directtools/DirectSession.py b/direct/src/directtools/DirectSession.py index 2947b29088..651a9addec 100644 --- a/direct/src/directtools/DirectSession.py +++ b/direct/src/directtools/DirectSession.py @@ -5,6 +5,7 @@ from panda3d.core import ( ConfigVariableBool, ConfigVariableString, CSDefault, + GraphicsWindow, NodePath, Point3, TextNode, @@ -35,19 +36,33 @@ from direct.gui import OnscreenText from direct.interval.IntervalGlobal import Func, Sequence from direct.task.TaskManagerGlobal import taskMgr from direct.showbase.MessengerGlobal import messenger +from direct.showbase import ShowBaseGlobal +from direct.showbase.ShowBaseGlobal import ShowBase, hidden +import builtins + +base: ShowBase class DirectSession(DirectObject): # post this to the bboard to make sure DIRECT doesn't turn on DIRECTdisablePost = 'disableDIRECT' + cam: NodePath + camera: NodePath + oobeCamera: NodePath + def __init__(self): # Establish a global pointer to the direct object early on # so dependant classes can access it in their code - __builtins__["direct"] = base.direct = self + global direct, base + base = ShowBaseGlobal.base + base.direct = self + setattr(builtins, 'direct', self) + ShowBaseGlobal.direct = self + # These come early since they are used later on - self.group = render.attachNewNode('DIRECT') + self.group = base.render.attachNewNode('DIRECT') self.font = TextNode.getDefaultFont() self.fEnabled = 0 self.fEnabledLight = 0 @@ -57,7 +72,7 @@ class DirectSession(DirectObject): self.drList = DisplayRegionList() self.iRayList = [x.iRay for x in self.drList] self.dr = self.drList[0] - self.win = base.win + self.win: GraphicsWindow = base.win self.camera = base.camera self.cam = base.cam self.camNode = base.camNode @@ -70,7 +85,7 @@ class DirectSession(DirectObject): self.useObjectHandles() self.grid = DirectGrid() self.grid.disable() - self.lights = DirectLights(base.direct.group) + self.lights = DirectLights(self.group) # Create some default lights self.lights.createDefaultLights() # But turn them off @@ -308,13 +323,16 @@ class DirectSession(DirectObject): if base.wantTk: from direct.tkpanels import DirectSessionPanel self.panel = DirectSessionPanel.DirectSessionPanel(parent = base.tkRoot) - try: + + clusterMode: str + if hasattr(builtins, 'clusterMode'): # Has the clusterMode been set externally (i.e. via the # bootstrap application? - self.clusterMode = clusterMode - except NameError: + clusterMode = builtins.clusterMode + else: # Has the clusterMode been set via a config variable? - self.clusterMode = ConfigVariableString("cluster-mode", '').value + clusterMode = ConfigVariableString("cluster-mode", '').value + self.clusterMode = clusterMode if self.clusterMode == 'client': from direct.cluster.ClusterClient import createClusterClient @@ -325,7 +343,7 @@ class DirectSession(DirectObject): else: from direct.cluster.ClusterClient import DummyClusterClient self.cluster = DummyClusterClient() - __builtins__['cluster'] = self.cluster + setattr(builtins, 'cluster', self.cluster) def addPassThroughKey(self,key): @@ -412,10 +430,10 @@ class DirectSession(DirectObject): if self.oobeMode: # Position a target point to lerp the oobe camera to - base.direct.cameraControl.camManipRef.setPosHpr(self.trueCamera, 0, 0, 0, 0, 0, 0) + self.cameraControl.camManipRef.setPosHpr(self.trueCamera, 0, 0, 0, 0, 0, 0) ival = self.oobeCamera.posHprInterval( 2.0, pos = Point3(0), hpr = Vec3(0), - other = base.direct.cameraControl.camManipRef, + other = self.cameraControl.camManipRef, blendType = 'easeInOut') ival = Sequence(ival, Func(self.endOOBE), name = 'oobeTransition') ival.start() @@ -432,20 +450,20 @@ class DirectSession(DirectObject): # Put camera under new oobe camera self.cam.reparentTo(self.oobeCamera) # Position a target point to lerp the oobe camera to - base.direct.cameraControl.camManipRef.setPos( + self.cameraControl.camManipRef.setPos( self.trueCamera, Vec3(-2, -20, 5)) - base.direct.cameraControl.camManipRef.lookAt(self.trueCamera) + self.cameraControl.camManipRef.lookAt(self.trueCamera) ival = self.oobeCamera.posHprInterval( 2.0, pos = Point3(0), hpr = Vec3(0), - other = base.direct.cameraControl.camManipRef, + other = self.cameraControl.camManipRef, blendType = 'easeInOut') ival = Sequence(ival, Func(self.beginOOBE), name = 'oobeTransition') ival.start() def beginOOBE(self): # Make sure we've reached our final destination - self.oobeCamera.setPosHpr(base.direct.cameraControl.camManipRef, 0, 0, 0, 0, 0, 0) - base.direct.camera = self.oobeCamera + self.oobeCamera.setPosHpr(self.cameraControl.camManipRef, 0, 0, 0, 0, 0, 0) + self.camera = self.oobeCamera self.oobeMode = 1 def endOOBE(self): @@ -453,7 +471,7 @@ class DirectSession(DirectObject): self.oobeCamera.setPosHpr(self.trueCamera, 0, 0, 0, 0, 0, 0) # Disable OOBE mode. self.cam.reparentTo(self.trueCamera) - base.direct.camera = self.trueCamera + self.camera = self.trueCamera # Get rid of ancillary node paths self.oobeVis.reparentTo(hidden) self.oobeCamera.reparentTo(hidden) @@ -501,7 +519,7 @@ class DirectSession(DirectObject): def inputHandler(self, input): if not hasattr(self, 'oobeMode') or self.oobeMode == 0: # [gjeon] change current camera dr, iRay, mouseWatcher accordingly to support multiple windows - if base.direct.manipulationControl.fMultiView: + if self.manipulationControl.fMultiView: # handling orphan events if self.fMouse1 and 'mouse1' not in input or\ self.fMouse2 and 'mouse2' not in input or\ @@ -518,7 +536,7 @@ class DirectSession(DirectObject): return if (self.fMouse1 or self.fMouse2 or self.fMouse3) and\ - input[4:7] != base.direct.camera.getName()[:3] and\ + input[4:7] != self.camera.getName()[:3] and\ input.endswith('-up'): # to handle orphan events return @@ -551,14 +569,14 @@ class DirectSession(DirectObject): self.cam = NodePath(winCtrl.camNode) self.camNode = winCtrl.camNode if hasattr(winCtrl, 'grid'): - base.direct.grid = winCtrl.grid - base.direct.dr = base.direct.drList[base.camList.index(NodePath(winCtrl.camNode))] - base.direct.iRay = base.direct.dr.iRay + self.grid = winCtrl.grid + self.dr = self.drList[base.camList.index(NodePath(winCtrl.camNode))] + self.iRay = self.dr.iRay base.mouseWatcher = winCtrl.mouseWatcher base.mouseWatcherNode = winCtrl.mouseWatcher.node() - base.direct.dr.mouseUpdate() + self.dr.mouseUpdate() DG.LE_showInOneCam(self.selectedNPReadout, self.camera.getName()) - base.direct.widget = base.direct.manipulationControl.widgetList[base.camList.index(NodePath(winCtrl.camNode))] + self.widget = self.manipulationControl.widgetList[base.camList.index(NodePath(winCtrl.camNode))] input = input[8:] # get rid of camera prefix if self.fAlt and 'alt' not in input and not input.endswith('-up'): @@ -683,20 +701,18 @@ class DirectSession(DirectObject): if not taskMgr.hasTaskNamed('resizeObjectHandles'): dnp = self.selected.last if dnp: - direct = base.direct - if self.manipulationControl.fMultiView: for i in range(3): - sf = 30.0 * direct.drList[i].orthoFactor + sf = 30.0 * self.drList[i].orthoFactor self.manipulationControl.widgetList[i].setDirectScalingFactor(sf) nodeCamDist = Vec3(dnp.getPos(base.camList[3])).length() - sf = 0.075 * nodeCamDist * math.tan(deg2Rad(direct.drList[3].fovV)) + sf = 0.075 * nodeCamDist * math.tan(deg2Rad(self.drList[3].fovV)) self.manipulationControl.widgetList[3].setDirectScalingFactor(sf) else: - nodeCamDist = Vec3(dnp.getPos(direct.camera)).length() - sf = 0.075 * nodeCamDist * math.tan(deg2Rad(direct.drList.getCurrentDr().fovV)) + nodeCamDist = Vec3(dnp.getPos(self.camera)).length() + sf = 0.075 * nodeCamDist * math.tan(deg2Rad(self.drList.getCurrentDr().fovV)) self.widget.setDirectScalingFactor(sf) return Task.cont @@ -755,7 +771,7 @@ class DirectSession(DirectObject): messenger.send('DIRECT_selectedNodePath_fMulti_fTag_fLEPane', [dnp, fMultiSelect, fSelectTag, fLEPane]) def followSelectedNodePathTask(self, state): - mCoa2Render = state.dnp.mCoa2Dnp * state.dnp.getMat(render) + mCoa2Render = state.dnp.mCoa2Dnp * state.dnp.getMat(base.render) decomposeMatrix(mCoa2Render, self.scale, self.hpr, self.pos, CSDefault) @@ -874,7 +890,7 @@ class DirectSession(DirectObject): if nodePath == 'None Given': # If nothing specified, try selected node path nodePath = self.selected.last - base.direct.select(nodePath) + self.select(nodePath) def fitTask(state, self = self): self.cameraControl.fitOnWidget() @@ -1061,7 +1077,7 @@ class DirectSession(DirectObject): def useObjectHandles(self): self.widget = self.manipulationControl.objectHandles - self.widget.reparentTo(base.direct.group) + self.widget.reparentTo(self.group) def hideSelectedNPReadout(self): self.selectedNPReadout.reparentTo(hidden) @@ -1167,14 +1183,14 @@ class DisplayRegionContext(DirectObject): self.camLens.setFov(hfov, vfov) def getWidth(self): - prop = base.direct.win.getProperties() + prop = ShowBaseGlobal.direct.win.getProperties() if prop.hasSize(): return prop.getXSize() else: return 640 def getHeight(self): - prop = base.direct.win.getProperties() + prop = ShowBaseGlobal.direct.win.getProperties() if prop.hasSize(): return prop.getYSize() else: @@ -1208,9 +1224,10 @@ class DisplayRegionContext(DirectObject): # Values for this frame # This ranges from -1 to 1 - if base.mouseWatcherNode and base.mouseWatcherNode.hasMouse(): - self.mouseX = base.mouseWatcherNode.getMouseX() - self.mouseY = base.mouseWatcherNode.getMouseY() + mouseWatcherNode = base.mouseWatcherNode + if mouseWatcherNode and mouseWatcherNode.hasMouse(): + self.mouseX = mouseWatcherNode.getMouseX() + self.mouseY = mouseWatcherNode.getMouseY() self.mouseX = (self.mouseX-self.originX)*self.scaleX self.mouseY = (self.mouseY-self.originY)*self.scaleY # Delta percent of window the mouse moved @@ -1262,6 +1279,9 @@ class DisplayRegionList(DirectObject): def __len__(self): return len(self.displayRegionList) + def __iter__(self): + return iter(self.displayRegionList) + def updateContext(self): self.contextTask(None) @@ -1296,7 +1316,7 @@ class DisplayRegionList(DirectObject): def getCurrentDr(self): if not self.tryToGetCurrentDr: - return base.direct.dr + return ShowBaseGlobal.direct.dr for dr in self.displayRegionList: if (dr.mouseX >= -1.0 and dr.mouseX <= 1.0 and dr.mouseY >= -1.0 and dr.mouseY <= 1.0): diff --git a/direct/src/leveleditor/LevelEditorUIBase.py b/direct/src/leveleditor/LevelEditorUIBase.py index 126bbf9f6b..87ccbb02ff 100755 --- a/direct/src/leveleditor/LevelEditorUIBase.py +++ b/direct/src/leveleditor/LevelEditorUIBase.py @@ -7,6 +7,7 @@ from direct.wxwidgets.WxPandaShell import WxPandaShell from direct.wxwidgets.WxSlider import WxSlider from direct.directtools.DirectSelection import SelectionRay from direct.showbase.MessengerGlobal import messenger +from direct.showbase import ShowBaseGlobal #from ViewPort import * from . import ObjectGlobals as OG @@ -88,7 +89,7 @@ class PandaTextDropTarget(wx.TextDropTarget): np = NodePath('temp') np.setPos(self.view.camera, hitPt) - if base.direct.manipulationControl.fGridSnap: + if ShowBaseGlobal.direct.manipulationControl.fGridSnap: snappedPos = self.view.grid.computeSnapPoint(np.getPos()) np.setPos(snappedPos) @@ -98,10 +99,10 @@ class PandaTextDropTarget(wx.TextDropTarget): # transform newobj to cursor position obj = self.editor.objectMgr.findObjectByNodePath(newobj) - action = ActionTransformObj(self.editor, obj[OG.OBJ_UID], Mat4(np.getMat())) - self.editor.actionMgr.push(action) + action2 = ActionTransformObj(self.editor, obj[OG.OBJ_UID], Mat4(np.getMat())) + self.editor.actionMgr.push(action2) np.remove() - action() + action2() iRay.collisionNodePath.removeNode() del iRay @@ -250,13 +251,13 @@ class LevelEditorUIBase(WxPandaShell): WxPandaShell.createMenu(self) def onGraphEditor(self, e): - if base.direct.selected.last is None: + if ShowBaseGlobal.direct.selected.last is None: dlg = wx.MessageDialog(None, 'Please select a object first.', 'NOTICE', wx.OK) dlg.ShowModal() dlg.Destroy() self.graphEditorMenuItem.Check(False) else: - currentObj = self.editor.objectMgr.findObjectByNodePath(base.direct.selected.last) + currentObj = self.editor.objectMgr.findObjectByNodePath(ShowBaseGlobal.direct.selected.last) self.graphEditorUI = GraphEditorUI(self, self.editor, currentObj) self.graphEditorUI.Show() self.graphEditorMenuItem.Check(True) @@ -298,7 +299,7 @@ class LevelEditorUIBase(WxPandaShell): degreeUI = CurveDegreeUI(self, -1, 'Curve Degree') degreeUI.ShowModal() degreeUI.Destroy() - base.direct.manipulationControl.disableManipulation() + ShowBaseGlobal.direct.manipulationControl.disableManipulation() self.editCurveMenuItem.Check(False) def onEditCurve(self, e): @@ -313,15 +314,15 @@ class LevelEditorUIBase(WxPandaShell): self.createCurveMenuItem.Check(False) self.onEditCurve(None) else: - if base.direct.selected.last is None: + if ShowBaseGlobal.direct.selected.last is None: dlg = wx.MessageDialog(None, 'Please select a curve first.', 'NOTICE', wx.OK) dlg.ShowModal() dlg.Destroy() self.editCurveMenuItem.Check(False) - if base.direct.selected.last is not None: - base.direct.manipulationControl.enableManipulation() + if ShowBaseGlobal.direct.selected.last is not None: + ShowBaseGlobal.direct.manipulationControl.enableManipulation() self.createCurveMenuItem.Check(False) - self.curveObj = self.editor.objectMgr.findObjectByNodePath(base.direct.selected.last) + self.curveObj = self.editor.objectMgr.findObjectByNodePath(ShowBaseGlobal.direct.selected.last) if self.curveObj[OG.OBJ_DEF].name == '__Curve__': self.editor.mode = self.editor.EDIT_CURVE_MODE self.editor.updateStatusReadout('Please press ENTER to end the curve editing.') @@ -339,8 +340,8 @@ class LevelEditorUIBase(WxPandaShell): def updateMenu(self): hotKeyDict = {} - for hotKey in base.direct.hotKeyMap.keys(): - desc = base.direct.hotKeyMap[hotKey] + for hotKey in ShowBaseGlobal.direct.hotKeyMap.keys(): + desc = ShowBaseGlobal.direct.hotKeyMap[hotKey] hotKeyDict[desc[1]] = hotKey for id in self.MENU_TEXTS.keys(): @@ -401,16 +402,16 @@ class LevelEditorUIBase(WxPandaShell): else: mpos = evt.GetPosition() - base.direct.fMouse3 = 0 + ShowBaseGlobal.direct.fMouse3 = 0 self.PopupMenu(self.contextMenu, mpos) def onKeyDownEvent(self, evt): if evt.GetKeyCode() == wx.WXK_ALT: - base.direct.fAlt = 1 + ShowBaseGlobal.direct.fAlt = 1 elif evt.GetKeyCode() == wx.WXK_CONTROL: - base.direct.fControl = 1 + ShowBaseGlobal.direct.fControl = 1 elif evt.GetKeyCode() == wx.WXK_SHIFT: - base.direct.fShift = 1 + ShowBaseGlobal.direct.fShift = 1 elif evt.GetKeyCode() == wx.WXK_UP: messenger.send('arrow_up') elif evt.GetKeyCode() == wx.WXK_DOWN: @@ -428,11 +429,11 @@ class LevelEditorUIBase(WxPandaShell): def onKeyUpEvent(self, evt): if evt.GetKeyCode() == wx.WXK_ALT: - base.direct.fAlt = 0 + ShowBaseGlobal.direct.fAlt = 0 elif evt.GetKeyCode() == wx.WXK_CONTROL: - base.direct.fControl = 0 + ShowBaseGlobal.direct.fControl = 0 elif evt.GetKeyCode() == wx.WXK_SHIFT: - base.direct.fShift = 0 + ShowBaseGlobal.direct.fShift = 0 elif evt.GetKeyCode() == wx.WXK_UP: messenger.send('arrow_up-up') elif evt.GetKeyCode() == wx.WXK_DOWN: @@ -473,8 +474,8 @@ class LevelEditorUIBase(WxPandaShell): input = 'control-%s'%chr(evt.GetKeyCode()) elif evt.GetKeyCode() < 256: input = chr(evt.GetKeyCode()) - if input in base.direct.hotKeyMap.keys(): - keyDesc = base.direct.hotKeyMap[input] + if input in ShowBaseGlobal.direct.hotKeyMap.keys(): + keyDesc = ShowBaseGlobal.direct.hotKeyMap[input] messenger.send(keyDesc[1]) def reset(self): @@ -533,12 +534,12 @@ class LevelEditorUIBase(WxPandaShell): def toggleGridSnap(self, evt): if self.gridSnapMenuItem.IsChecked(): - base.direct.manipulationControl.fGridSnap = 1 + ShowBaseGlobal.direct.manipulationControl.fGridSnap = 1 for grid in [self.perspView.grid, self.topView.grid, self.frontView.grid, self.leftView.grid]: grid.fXyzSnap = 1 else: - base.direct.manipulationControl.fGridSnap = 0 + ShowBaseGlobal.direct.manipulationControl.fGridSnap = 0 for grid in [self.perspView.grid, self.topView.grid, self.frontView.grid, self.leftView.grid]: grid.fXyzSnap = 0 @@ -589,7 +590,7 @@ class LevelEditorUIBase(WxPandaShell): self.contextMenu.AppendSeparator() def replaceObject(self, evt, all=False): - currObj = self.editor.objectMgr.findObjectByNodePath(base.direct.selected.last) + currObj = self.editor.objectMgr.findObjectByNodePath(ShowBaseGlobal.direct.selected.last) if currObj is None: print('No valid object is selected for replacement') return @@ -636,13 +637,13 @@ class GridSizeUI(wx.Dialog): vbox.Add(okButton, 1, wx.ALIGN_CENTER | wx.TOP | wx.BOTTOM, 5) self.SetSizer(vbox) - base.le.ui.bindKeyEvents(False) + ShowBaseGlobal.base.le.ui.bindKeyEvents(False) def onApply(self, evt): newSize = self.gridSizeSlider.GetValue() newSpacing = self.gridSpacingSlider.GetValue() self.parent.updateGrids(newSize, newSpacing) - base.le.ui.bindKeyEvents(True) + ShowBaseGlobal.base.le.ui.bindKeyEvents(True) self.Destroy() diff --git a/direct/src/leveleditor/LevelLoader.py b/direct/src/leveleditor/LevelLoader.py index 34a843dd61..cc5fb13c90 100755 --- a/direct/src/leveleditor/LevelLoader.py +++ b/direct/src/leveleditor/LevelLoader.py @@ -25,6 +25,9 @@ class LevelLoader(LevelLoaderBase): def initLoader(self): self.defaultPath = os.path.dirname(__file__) + + from direct.showbase import ShowBaseGlobal + base = ShowBaseGlobal.base base.objectPalette = ObjectPalette() base.protoPalette = ProtoPalette() base.objectHandler = ObjectHandler(None) diff --git a/direct/src/showbase/ShowBase.py b/direct/src/showbase/ShowBase.py index df2630c636..05d7612339 100644 --- a/direct/src/showbase/ShowBase.py +++ b/direct/src/showbase/ShowBase.py @@ -141,7 +141,7 @@ import importlib from direct.showbase import ExceptionVarDump from . import DirectObject from . import SfxPlayer -from typing import ClassVar +from typing import ClassVar, Optional if __debug__: from direct.showbase import GarbageReport from direct.directutil import DeltaProfiler @@ -166,6 +166,10 @@ class ShowBase(DirectObject.DirectObject): notify: ClassVar[Notifier] = directNotify.newCategory("ShowBase") guiItems: ClassVar[dict] + render2d: NodePath + aspect2d: NodePath + pixel2d: NodePath + def __init__(self, fStartDirect=True, windowType=None): """Opens a window, sets up a 3-D and several 2-D scene graphs, and everything else needed to render the scene graph to the window. diff --git a/direct/src/showbase/ShowBaseGlobal.py b/direct/src/showbase/ShowBaseGlobal.py index 58e7e69f58..a9fd35c51a 100644 --- a/direct/src/showbase/ShowBaseGlobal.py +++ b/direct/src/showbase/ShowBaseGlobal.py @@ -61,6 +61,8 @@ aspect2d = render2d.attachNewNode(PGTop("aspect2d")) #: A dummy scene graph that is not being rendered by anything. hidden = NodePath("hidden") +direct: "DirectSession" + # Set direct notify categories now that we have config directNotify.setDconfigLevels() diff --git a/direct/src/showutil/TexMemWatcher.py b/direct/src/showutil/TexMemWatcher.py index 3135c76b05..2ddd54cc59 100644 --- a/direct/src/showutil/TexMemWatcher.py +++ b/direct/src/showutil/TexMemWatcher.py @@ -22,6 +22,7 @@ from panda3d.core import ( WindowProperties, ) from direct.showbase.DirectObject import DirectObject +from direct.showbase import ShowBaseGlobal from direct.task.TaskManagerGlobal import taskMgr import math import copy @@ -109,7 +110,7 @@ class TexMemWatcher(DirectObject): # This is the maximum number of bitmask rows (within # self.limit) to allocate for packing. This controls the # value assigned to self.quantize in repack(). - self.maxHeight = base.config.GetInt('tex-mem-max-height', 300) + self.maxHeight = ConfigVariableInt('tex-mem-max-height', 300).value # The total number of texture bytes tracked, including overflow. self.totalSize = 0 @@ -122,6 +123,7 @@ class TexMemWatcher(DirectObject): self.placedQSize = 0 # If no GSG is specified, use the main GSG. + base = ShowBaseGlobal.base if gsg is None: gsg = base.win.getGsg() elif isinstance(gsg, GraphicsOutput): @@ -150,7 +152,7 @@ class TexMemWatcher(DirectObject): # Set this to tinydisplay if you're running on a machine with # limited texture memory. That way you won't compete for # texture memory with the main scene. - moduleName = base.config.GetString('tex-mem-pipe', '') + moduleName = ConfigVariableString('tex-mem-pipe', '').value if moduleName: self.pipe = base.makeModulePipe(moduleName) @@ -202,7 +204,7 @@ class TexMemWatcher(DirectObject): # How frequently should the texture memory window check for # state changes? - updateInterval = base.config.GetDouble("tex-mem-update-interval", 0.5) + updateInterval = ConfigVariableDouble("tex-mem-update-interval", 0.5).value self.task = taskMgr.doMethodLater(updateInterval, self.updateTextures, 'TexMemWatcher') self.setLimit(limit) @@ -380,7 +382,7 @@ class TexMemWatcher(DirectObject): self.cleanedUp = True # Remove the window. - base.graphicsEngine.removeWindow(self.win) + self.win.engine.removeWindow(self.win) self.win = None self.gsg = None self.pipe = None diff --git a/direct/src/tkpanels/DirectSessionPanel.py b/direct/src/tkpanels/DirectSessionPanel.py index b3e521deea..95eaad784d 100644 --- a/direct/src/tkpanels/DirectSessionPanel.py +++ b/direct/src/tkpanels/DirectSessionPanel.py @@ -20,6 +20,7 @@ from direct.tkwidgets import VectorWidgets from direct.tkwidgets import SceneGraphExplorer from direct.tkwidgets import MemoryExplorer from direct.task.TaskManagerGlobal import taskMgr +from direct.showbase import ShowBaseGlobal from .TaskManagerPanel import TaskManagerWidget import Pmw import tkinter as tk @@ -44,8 +45,8 @@ class DirectSessionPanel(AppShell): AppShell.__init__(self, parent) # Active light - if len(base.direct.lights) > 0: - name = base.direct.lights.getNameList()[0] + if len(ShowBaseGlobal.direct.lights) > 0: + name = ShowBaseGlobal.direct.lights.getNameList()[0] self.lightMenu.selectitem(name) self.selectLightNamed(name) else: @@ -62,14 +63,14 @@ class DirectSessionPanel(AppShell): # Initialize state # Dictionary keeping track of all node paths selected so far self.nodePathDict = {} - self.nodePathDict['widget'] = base.direct.widget + self.nodePathDict['widget'] = ShowBaseGlobal.direct.widget self.nodePathNames = ['widget'] # Dictionary keeping track of all jb node paths selected so far self.jbNodePathDict = {} self.jbNodePathDict['none'] = 'No Node Path' - self.jbNodePathDict['widget'] = base.direct.widget - self.jbNodePathDict['camera'] = base.direct.camera + self.jbNodePathDict['widget'] = ShowBaseGlobal.direct.widget + self.jbNodePathDict['camera'] = ShowBaseGlobal.direct.camera self.jbNodePathNames = ['camera', 'selected', 'none'] # Set up event hooks @@ -93,7 +94,7 @@ class DirectSessionPanel(AppShell): self.menuBar.addmenu('DIRECT', 'Direct Session Panel Operations') self.directEnabled = tk.BooleanVar() - self.directEnabled.set(1) + self.directEnabled.set(True) self.menuBar.addmenuitem('DIRECT', 'checkbutton', 'DIRECT Enabled', label = 'Enable', @@ -101,7 +102,7 @@ class DirectSessionPanel(AppShell): command = self.toggleDirect) self.directGridEnabled = tk.BooleanVar() - self.directGridEnabled.set(base.direct.grid.isEnabled()) + self.directGridEnabled.set(ShowBaseGlobal.direct.grid.isEnabled()) self.menuBar.addmenuitem('DIRECT', 'checkbutton', 'DIRECT Grid Enabled', label = 'Enable Grid', @@ -111,16 +112,16 @@ class DirectSessionPanel(AppShell): self.menuBar.addmenuitem('DIRECT', 'command', 'Toggle Object Handles Visability', label = 'Toggle Widget Viz', - command = base.direct.toggleWidgetVis) + command = ShowBaseGlobal.direct.toggleWidgetVis) self.menuBar.addmenuitem( 'DIRECT', 'command', 'Toggle Widget Move/COA Mode', label = 'Toggle Widget Mode', - command = base.direct.manipulationControl.toggleObjectHandlesMode) + command = ShowBaseGlobal.direct.manipulationControl.toggleObjectHandlesMode) self.directWidgetOnTop = tk.BooleanVar() - self.directWidgetOnTop.set(0) + self.directWidgetOnTop.set(False) self.menuBar.addmenuitem('DIRECT', 'checkbutton', 'DIRECT Widget On Top', label = 'Widget On Top', @@ -130,7 +131,7 @@ class DirectSessionPanel(AppShell): self.menuBar.addmenuitem('DIRECT', 'command', 'Deselect All', label = 'Deselect All', - command = base.direct.deselectAll) + command = ShowBaseGlobal.direct.deselectAll) # Get a handle to the menu frame menuFrame = self.menuFrame @@ -150,8 +151,8 @@ class DirectSessionPanel(AppShell): self.bind(self.nodePathMenu, 'Select node path to manipulate') self.undoButton = tk.Button(menuFrame, text = 'Undo', - command = base.direct.undo) - if base.direct.undoList: + command = ShowBaseGlobal.direct.undo) + if ShowBaseGlobal.direct.undoList: self.undoButton['state'] = 'normal' else: self.undoButton['state'] = 'disabled' @@ -159,8 +160,8 @@ class DirectSessionPanel(AppShell): self.bind(self.undoButton, 'Undo last operation') self.redoButton = tk.Button(menuFrame, text = 'Redo', - command = base.direct.redo) - if base.direct.redoList: + command = ShowBaseGlobal.direct.redo) + if ShowBaseGlobal.direct.redoList: self.redoButton['state'] = 'normal' else: self.redoButton['state'] = 'disabled' @@ -177,7 +178,7 @@ class DirectSessionPanel(AppShell): # Scene Graph Explorer self.SGE = SceneGraphExplorer.SceneGraphExplorer( - sgeFrame, nodePath = render, + sgeFrame, nodePath = ShowBaseGlobal.base.render, scrolledCanvas_hull_width = 250, scrolledCanvas_hull_height = 300) self.SGE.pack(fill = tk.BOTH, expand = 1) @@ -218,7 +219,7 @@ class DirectSessionPanel(AppShell): tk.Label(drFrame, text = 'Display Region', font=('MSSansSerif', 14, 'bold')).pack(expand = 0) - nameList = ['Display Region ' + repr(x) for x in range(len(base.direct.drList))] + nameList = ['Display Region ' + repr(x) for x in range(len(ShowBaseGlobal.direct.drList))] self.drMenu = Pmw.ComboBox( drFrame, labelpos = tk.W, label_text = 'Display Region:', entry_width = 20, @@ -264,7 +265,7 @@ class DirectSessionPanel(AppShell): frame = tk.Frame(fovFrame) self.lockedFov = tk.BooleanVar() - self.lockedFov.set(1) + self.lockedFov.set(True) self.lockedFovButton = tk.Checkbutton( frame, text = 'Locked', @@ -289,25 +290,25 @@ class DirectSessionPanel(AppShell): self.toggleBackfaceButton = tk.Button( toggleFrame, text = 'Backface', - command = base.toggleBackface) + command = ShowBaseGlobal.base.toggleBackface) self.toggleBackfaceButton.pack(side = tk.LEFT, fill = tk.X, expand = 1) self.toggleLightsButton = tk.Button( toggleFrame, text = 'Lights', - command = base.direct.lights.toggle) + command = ShowBaseGlobal.direct.lights.toggle) self.toggleLightsButton.pack(side = tk.LEFT, fill = tk.X, expand = 1) self.toggleTextureButton = tk.Button( toggleFrame, text = 'Texture', - command = base.toggleTexture) + command = ShowBaseGlobal.base.toggleTexture) self.toggleTextureButton.pack(side = tk.LEFT, fill = tk.X, expand = 1) self.toggleWireframeButton = tk.Button( toggleFrame, text = 'Wireframe', - command = base.toggleWireframe) + command = ShowBaseGlobal.base.toggleWireframe) self.toggleWireframeButton.pack(fill = tk.X, expand = 1) toggleFrame.pack(side = tk.LEFT, fill = tk.X, expand = 1) @@ -354,7 +355,7 @@ class DirectSessionPanel(AppShell): mainSwitchFrame.pack(fill = tk.X, expand = 0) # Widget to select a light to configure - nameList = base.direct.lights.getNameList() + nameList = ShowBaseGlobal.direct.lights.getNameList() lightMenuFrame = tk.Frame(lightFrame) self.lightMenu = Pmw.ComboBox( @@ -510,36 +511,36 @@ class DirectSessionPanel(AppShell): gridPage, text = 'Grid Spacing', min = 0.1, - value = base.direct.grid.getGridSpacing()) - self.gridSpacing['command'] = base.direct.grid.setGridSpacing + value = ShowBaseGlobal.direct.grid.getGridSpacing()) + self.gridSpacing['command'] = ShowBaseGlobal.direct.grid.setGridSpacing self.gridSpacing.pack(fill = tk.X, expand = 0) self.gridSize = Floater.Floater( gridPage, text = 'Grid Size', min = 1.0, - value = base.direct.grid.getGridSize()) - self.gridSize['command'] = base.direct.grid.setGridSize + value = ShowBaseGlobal.direct.grid.getGridSize()) + self.gridSize['command'] = ShowBaseGlobal.direct.grid.setGridSize self.gridSize.pack(fill = tk.X, expand = 0) self.gridSnapAngle = Dial.AngleDial( gridPage, text = 'Snap Angle', style = 'mini', - value = base.direct.grid.getSnapAngle()) - self.gridSnapAngle['command'] = base.direct.grid.setSnapAngle + value = ShowBaseGlobal.direct.grid.getSnapAngle()) + self.gridSnapAngle['command'] = ShowBaseGlobal.direct.grid.setSnapAngle self.gridSnapAngle.pack(fill = tk.X, expand = 0) def createDevicePage(self, devicePage): tk.Label(devicePage, text = 'DEVICES', font=('MSSansSerif', 14, 'bold')).pack(expand = 0) - if base.direct.joybox is not None: + if ShowBaseGlobal.direct.joybox is not None: joyboxFrame = tk.Frame(devicePage, borderwidth = 2, relief = 'sunken') tk.Label(joyboxFrame, text = 'Joybox', font=('MSSansSerif', 14, 'bold')).pack(expand = 0) self.enableJoybox = tk.BooleanVar() - self.enableJoybox.set(1) + self.enableJoybox.set(True) self.enableJoyboxButton = tk.Checkbutton( joyboxFrame, text = 'Enabled/Disabled', @@ -581,7 +582,7 @@ class DirectSessionPanel(AppShell): hull_relief = tk.RIDGE, hull_borderwidth = 2, min = 1.0, max = 100.0) self.jbXyzSF['command'] = ( - lambda v: base.direct.joybox.setXyzMultiplier(v)) + lambda v: ShowBaseGlobal.direct.joybox.setXyzMultiplier(v)) self.jbXyzSF.pack(fill = tk.X, expand = 0) self.bind(self.jbXyzSF, 'Set joybox XYZ speed multiplier') @@ -592,7 +593,7 @@ class DirectSessionPanel(AppShell): hull_relief = tk.RIDGE, hull_borderwidth = 2, min = 1.0, max = 100.0) self.jbHprSF['command'] = ( - lambda v: base.direct.joybox.setHprMultiplier(v)) + lambda v: ShowBaseGlobal.direct.joybox.setHprMultiplier(v)) self.jbHprSF.pack(fill = tk.X, expand = 0) self.bind(self.jbHprSF, 'Set joybox HPR speed multiplier') @@ -604,30 +605,30 @@ class DirectSessionPanel(AppShell): def createMemPage(self, memPage): self.MemExp = MemoryExplorer.MemoryExplorer( - memPage, nodePath = render, + memPage, nodePath = ShowBaseGlobal.base.render, scrolledCanvas_hull_width = 250, scrolledCanvas_hull_height = 250) self.MemExp.pack(fill = tk.BOTH, expand = 1) def toggleDirect(self): if self.directEnabled.get(): - base.direct.enable() + ShowBaseGlobal.direct.enable() else: - base.direct.disable() + ShowBaseGlobal.direct.disable() def toggleDirectGrid(self): if self.directGridEnabled.get(): - base.direct.grid.enable() + ShowBaseGlobal.direct.grid.enable() else: - base.direct.grid.disable() + ShowBaseGlobal.direct.grid.disable() def toggleWidgetOnTop(self): if self.directWidgetOnTop.get(): - base.direct.widget.setBin('gui-popup', 0) - base.direct.widget.setDepthTest(0) + ShowBaseGlobal.direct.widget.setBin('gui-popup', 0) + ShowBaseGlobal.direct.widget.setDepthTest(0) else: - base.direct.widget.clearBin() - base.direct.widget.setDepthTest(1) + ShowBaseGlobal.direct.widget.clearBin() + ShowBaseGlobal.direct.widget.setDepthTest(1) def selectedNodePathHook(self, nodePath): # Make sure node path is in nodePathDict @@ -657,7 +658,7 @@ class DirectSessionPanel(AppShell): # Did we finally get something? if nodePath is not None: # Yes, select it! - base.direct.select(nodePath) + ShowBaseGlobal.direct.select(nodePath) def addNodePath(self, nodePath): self.addNodePathToDict(nodePath, self.nodePathNames, @@ -665,25 +666,25 @@ class DirectSessionPanel(AppShell): def selectJBModeNamed(self, name): if name == 'Joe Mode': - base.direct.joybox.joeMode() + ShowBaseGlobal.direct.joybox.joeMode() elif name == 'Drive Mode': - base.direct.joybox.driveMode() + ShowBaseGlobal.direct.joybox.driveMode() elif name == 'Orbit Mode': - base.direct.joybox.orbitMode() + ShowBaseGlobal.direct.joybox.orbitMode() elif name == 'Look At Mode': - base.direct.joybox.lookAtMode() + ShowBaseGlobal.direct.joybox.lookAtMode() elif name == 'Look Around Mode': - base.direct.joybox.lookAroundMode() + ShowBaseGlobal.direct.joybox.lookAroundMode() elif name == 'Walkthru Mode': - base.direct.joybox.walkthruMode() + ShowBaseGlobal.direct.joybox.walkthruMode() elif name == 'Demo Mode': - base.direct.joybox.demoMode() + ShowBaseGlobal.direct.joybox.demoMode() elif name == 'HPRXYZ Mode': - base.direct.joybox.hprXyzMode() + ShowBaseGlobal.direct.joybox.hprXyzMode() def selectJBNodePathNamed(self, name): if name == 'selected': - nodePath = base.direct.selected.last + nodePath = ShowBaseGlobal.direct.selected.last # Add Combo box entry for this selected object self.addJBNodePath(nodePath) else: @@ -708,9 +709,9 @@ class DirectSessionPanel(AppShell): if nodePath is not None: # Yes, select it! if nodePath == 'No Node Path': - base.direct.joybox.setNodePath(None) + ShowBaseGlobal.direct.joybox.setNodePath(None) else: - base.direct.joybox.setNodePath(nodePath) + ShowBaseGlobal.direct.joybox.setNodePath(nodePath) def addJBNodePath(self, nodePath): self.addNodePathToDict(nodePath, self.jbNodePathNames, @@ -741,14 +742,13 @@ class DirectSessionPanel(AppShell): self.setBackgroundColorVec((r, g, b)) def setBackgroundColorVec(self, color): - base.setBackgroundColor(color[0]/255.0, - color[1]/255.0, - color[2]/255.0) + ShowBaseGlobal.base.setBackgroundColor( + color[0] / 255.0, color[1] / 255.0, color[2] / 255.0) def selectDisplayRegionNamed(self, name): if name.find('Display Region ') >= 0: drIndex = int(name[-1:]) - self.activeDisplayRegion = base.direct.drList[drIndex] + self.activeDisplayRegion = ShowBaseGlobal.direct.drList[drIndex] else: self.activeDisplayRegion = None # Make sure info is current @@ -758,13 +758,13 @@ class DirectSessionPanel(AppShell): dr = self.activeDisplayRegion if dr: dr.camLens.setNear(near) - cluster('base.camLens.setNear(%f)' % near, 0) + ShowBaseGlobal.direct.cluster('base.camLens.setNear(%f)' % near, 0) def setFar(self, far): dr = self.activeDisplayRegion if dr: dr.camLens.setFar(far) - cluster('base.camLens.setFar(%f)' % far, 0) + ShowBaseGlobal.direct.cluster('base.camLens.setFar(%f)' % far, 0) def setHFov(self, hFov): dr = self.activeDisplayRegion @@ -802,10 +802,10 @@ class DirectSessionPanel(AppShell): # Lights # def selectLightNamed(self, name): # See if light exists - self.activeLight = base.direct.lights[name] + self.activeLight = ShowBaseGlobal.direct.lights[name] # If not...create new one if self.activeLight is None: - self.activeLight = base.direct.lights.create(name) + self.activeLight = ShowBaseGlobal.direct.lights.create(name) # Do we have a valid light at this point? if self.activeLight: light = self.activeLight.getLight() @@ -820,28 +820,28 @@ class DirectSessionPanel(AppShell): else: # Restore valid data listbox = self.lightMenu.component('scrolledlist') - listbox.setlist(base.direct.lights.getNameList()) - if len(base.direct.lights) > 0: - self.lightMenu.selectitem(base.direct.lights.getNameList()[0]) + listbox.setlist(ShowBaseGlobal.direct.lights.getNameList()) + if len(ShowBaseGlobal.direct.lights) > 0: + self.lightMenu.selectitem(ShowBaseGlobal.direct.lights.getNameList()[0]) # Make sure info is current self.updateLightInfo() def addAmbient(self): - return base.direct.lights.create('ambient') + return ShowBaseGlobal.direct.lights.create('ambient') def addDirectional(self): - return base.direct.lights.create('directional') + return ShowBaseGlobal.direct.lights.create('directional') def addPoint(self): - return base.direct.lights.create('point') + return ShowBaseGlobal.direct.lights.create('point') def addSpot(self): - return base.direct.lights.create('spot') + return ShowBaseGlobal.direct.lights.create('spot') def addLight(self, light): # Make list reflect current list of lights listbox = self.lightMenu.component('scrolledlist') - listbox.setlist(base.direct.lights.getNameList()) + listbox.setlist(ShowBaseGlobal.direct.lights.getNameList()) # Select the newly added light self.lightMenu.selectitem(light.getName()) # And show corresponding page @@ -849,16 +849,16 @@ class DirectSessionPanel(AppShell): def toggleLights(self): if self.enableLights.get(): - base.direct.lights.allOn() + ShowBaseGlobal.direct.lights.allOn() else: - base.direct.lights.allOff() + ShowBaseGlobal.direct.lights.allOff() def toggleActiveLight(self): if self.activeLight: if self.lightActive.get(): - base.direct.lights.setOn(self.activeLight) + ShowBaseGlobal.direct.lights.setOn(self.activeLight) else: - base.direct.lights.setOff(self.activeLight) + ShowBaseGlobal.direct.lights.setOff(self.activeLight) def setLightColor(self, color): if self.activeLight: @@ -893,22 +893,22 @@ class DirectSessionPanel(AppShell): ## GRID CONTROLS ## def toggleGrid(self): if self.enableGrid.get(): - base.direct.grid.enable() + ShowBaseGlobal.direct.grid.enable() else: - base.direct.grid.disable() + ShowBaseGlobal.direct.grid.disable() def toggleXyzSnap(self): - base.direct.grid.setXyzSnap(self.xyzSnap.get()) + ShowBaseGlobal.direct.grid.setXyzSnap(self.xyzSnap.get()) def toggleHprSnap(self): - base.direct.grid.setHprSnap(self.hprSnap.get()) + ShowBaseGlobal.direct.grid.setHprSnap(self.hprSnap.get()) ## DEVICE CONTROLS def toggleJoybox(self): if self.enableJoybox.get(): - base.direct.joybox.enable() + ShowBaseGlobal.direct.joybox.enable() else: - base.direct.joybox.disable() + ShowBaseGlobal.direct.joybox.disable() ## UPDATE INFO ## def updateInfo(self, page = 'Environment'): @@ -920,7 +920,7 @@ class DirectSessionPanel(AppShell): self.updateGridInfo() def updateEnvironmentInfo(self): - bkgrdColor = base.getBackgroundColor() * 255.0 + bkgrdColor = ShowBaseGlobal.base.getBackgroundColor() * 255.0 self.backgroundColor.set([bkgrdColor[0], bkgrdColor[1], bkgrdColor[2], @@ -936,6 +936,7 @@ class DirectSessionPanel(AppShell): def updateLightInfo(self, page = None): # Set main lighting button + render = ShowBaseGlobal.base.render self.enableLights.set( render.node().hasAttrib(LightAttrib.getClassType())) @@ -974,16 +975,16 @@ class DirectSessionPanel(AppShell): self.pQuadraticAttenuation.set(att[2], 0) def updateGridInfo(self): - self.enableGrid.set(base.direct.grid.isEnabled()) - self.xyzSnap.set(base.direct.grid.getXyzSnap()) - self.hprSnap.set(base.direct.grid.getHprSnap()) - self.gridSpacing.set(base.direct.grid.getGridSpacing(), 0) - self.gridSize.set(base.direct.grid.getGridSize(), 0) - self.gridSnapAngle.set(base.direct.grid.getSnapAngle(), 0) + self.enableGrid.set(ShowBaseGlobal.direct.grid.isEnabled()) + self.xyzSnap.set(ShowBaseGlobal.direct.grid.getXyzSnap()) + self.hprSnap.set(ShowBaseGlobal.direct.grid.getHprSnap()) + self.gridSpacing.set(ShowBaseGlobal.direct.grid.getGridSpacing(), 0) + self.gridSize.set(ShowBaseGlobal.direct.grid.getGridSize(), 0) + self.gridSnapAngle.set(ShowBaseGlobal.direct.grid.getSnapAngle(), 0) # UNDO/REDO def pushUndo(self, fResetRedo = 1): - base.direct.pushUndo([self['nodePath']]) + ShowBaseGlobal.direct.pushUndo([self['nodePath']]) def undoHook(self, nodePathList = []): pass @@ -997,7 +998,7 @@ class DirectSessionPanel(AppShell): self.undoButton.configure(state = 'disabled') def pushRedo(self): - base.direct.pushRedo([self['nodePath']]) + ShowBaseGlobal.direct.pushRedo([self['nodePath']]) def redoHook(self, nodePathList = []): pass diff --git a/direct/src/tkpanels/Inspector.py b/direct/src/tkpanels/Inspector.py index 15cc04dc46..707bc038c3 100644 --- a/direct/src/tkpanels/Inspector.py +++ b/direct/src/tkpanels/Inspector.py @@ -31,6 +31,8 @@ def inspect(anObject): ### private +_InspectorMap: dict[str, str] + def inspectorFor(anObject): typeName = type(anObject).__name__.capitalize() + 'Type' @@ -396,7 +398,8 @@ class InspectorWindow: self.listWidget.component('listbox').focus_set() def showHelp(self): - help = tk.Toplevel(base.tkRoot) + from direct.showbase import ShowBaseGlobal + help = tk.Toplevel(ShowBaseGlobal.base.tkRoot) help.title("Inspector Help") frame = tk.Frame(help) frame.pack() diff --git a/direct/src/tkpanels/Placer.py b/direct/src/tkpanels/Placer.py index 6fdf0767f1..2549fee2a1 100644 --- a/direct/src/tkpanels/Placer.py +++ b/direct/src/tkpanels/Placer.py @@ -9,6 +9,7 @@ from direct.tkwidgets import Dial from direct.tkwidgets import Floater from direct.directtools.DirectGlobals import ZERO_VEC, UNIT_VEC from direct.showbase.MessengerGlobal import messenger +from direct.showbase import ShowBaseGlobal from direct.task.TaskManagerGlobal import taskMgr import Pmw import tkinter as tk @@ -28,7 +29,7 @@ class Placer(AppShell): INITOPT = Pmw.INITOPT optiondefs = ( ('title', self.appname, None), - ('nodePath', base.direct.camera, None), + ('nodePath', ShowBaseGlobal.direct.camera, None), ) self.defineoptions(kw, optiondefs) @@ -39,23 +40,23 @@ class Placer(AppShell): def appInit(self): # Initialize state - self.tempCS = base.direct.group.attachNewNode('placerTempCS') - self.orbitFromCS = base.direct.group.attachNewNode( + self.tempCS = ShowBaseGlobal.direct.group.attachNewNode('placerTempCS') + self.orbitFromCS = ShowBaseGlobal.direct.group.attachNewNode( 'placerOrbitFromCS') - self.orbitToCS = base.direct.group.attachNewNode('placerOrbitToCS') + self.orbitToCS = ShowBaseGlobal.direct.group.attachNewNode('placerOrbitToCS') self.refCS = self.tempCS # Dictionary keeping track of all node paths manipulated so far self.nodePathDict = {} - self.nodePathDict['camera'] = base.direct.camera - self.nodePathDict['widget'] = base.direct.widget + self.nodePathDict['camera'] = ShowBaseGlobal.direct.camera + self.nodePathDict['widget'] = ShowBaseGlobal.direct.widget self.nodePathNames = ['camera', 'widget', 'selected'] self.refNodePathDict = {} self.refNodePathDict['parent'] = self['nodePath'].getParent() self.refNodePathDict['render'] = render - self.refNodePathDict['camera'] = base.direct.camera - self.refNodePathDict['widget'] = base.direct.widget + self.refNodePathDict['camera'] = ShowBaseGlobal.direct.camera + self.refNodePathDict['widget'] = ShowBaseGlobal.direct.widget self.refNodePathNames = ['parent', 'self', 'render', 'camera', 'widget', 'selected'] @@ -103,12 +104,12 @@ class Placer(AppShell): 'Placer', 'command', 'Toggle widget visability', label = 'Toggle Widget Vis', - command = base.direct.toggleWidgetVis) + command = ShowBaseGlobal.direct.toggleWidgetVis) self.menuBar.addmenuitem( 'Placer', 'command', 'Toggle widget manipulation mode', label = 'Toggle Widget Mode', - command = base.direct.manipulationControl.toggleObjectHandlesMode) + command = ShowBaseGlobal.direct.manipulationControl.toggleObjectHandlesMode) # Get a handle to the menu frame menuFrame = self.menuFrame @@ -145,8 +146,8 @@ class Placer(AppShell): self.bind(self.refNodePathMenu, 'Select relative node path') self.undoButton = tk.Button(menuFrame, text = 'Undo', - command = base.direct.undo) - if base.direct.undoList: + command = ShowBaseGlobal.direct.undo) + if ShowBaseGlobal.direct.undoList: self.undoButton['state'] = 'normal' else: self.undoButton['state'] = 'disabled' @@ -154,8 +155,8 @@ class Placer(AppShell): self.bind(self.undoButton, 'Undo last operation') self.redoButton = tk.Button(menuFrame, text = 'Redo', - command = base.direct.redo) - if base.direct.redoList: + command = ShowBaseGlobal.direct.redo) + if ShowBaseGlobal.direct.redoList: self.redoButton['state'] = 'normal' else: self.redoButton['state'] = 'disabled' @@ -394,7 +395,7 @@ class Placer(AppShell): # Add Combo box entry for the initial node path self.addNodePath(nodePath) elif name == 'selected': - nodePath = base.direct.selected.last + nodePath = ShowBaseGlobal.direct.selected.last # Add Combo box entry for this selected object self.addNodePath(nodePath) else: @@ -417,7 +418,7 @@ class Placer(AppShell): else: if name == 'widget': # Record relationship between selected nodes and widget - base.direct.selected.getWrtAll() + ShowBaseGlobal.direct.selected.getWrtAll() # Update active node path self.setActiveNodePath(nodePath) @@ -449,7 +450,7 @@ class Placer(AppShell): if name == 'self': nodePath = self.tempCS elif name == 'selected': - nodePath = base.direct.selected.last + nodePath = ShowBaseGlobal.direct.selected.last # Add Combo box entry for this selected object self.addRefNodePath(nodePath) elif name == 'parent': @@ -560,13 +561,13 @@ class Placer(AppShell): elif self.movementMode == 'Orbit:': self.xformOrbit(value, axis) if self.nodePathMenu.get() == 'widget': - if base.direct.manipulationControl.fSetCoa: + if ShowBaseGlobal.direct.manipulationControl.fSetCoa: # Update coa based on current widget position - base.direct.selected.last.mCoa2Dnp.assign( - base.direct.widget.getMat(base.direct.selected.last)) + ShowBaseGlobal.direct.selected.last.mCoa2Dnp.assign( + ShowBaseGlobal.direct.widget.getMat(ShowBaseGlobal.direct.selected.last)) else: # Move the objects with the widget - base.direct.selected.moveWrtWidgetAll() + ShowBaseGlobal.direct.selected.moveWrtWidgetAll() def xformStart(self, data): # Record undo point @@ -575,7 +576,7 @@ class Placer(AppShell): if self.nodePathMenu.get() == 'widget': taskMgr.remove('followSelectedNodePath') # Record relationship between selected nodes and widget - base.direct.selected.getWrtAll() + ShowBaseGlobal.direct.selected.getWrtAll() # Record initial state self.deltaHpr = self['nodePath'].getHpr(self.refCS) # Update placer to reflect new state @@ -590,7 +591,7 @@ class Placer(AppShell): # If moving widget restart follow task if self.nodePathMenu.get() == 'widget': # Restart followSelectedNodePath task - base.direct.manipulationControl.spawnFollowSelectedNodePathTask() + ShowBaseGlobal.direct.manipulationControl.spawnFollowSelectedNodePathTask() def xformRelative(self, value, axis): nodePath = self['nodePath'] @@ -729,7 +730,7 @@ class Placer(AppShell): self.xformStop(None) def pushUndo(self, fResetRedo = 1): - base.direct.pushUndo([self['nodePath']]) + ShowBaseGlobal.direct.pushUndo([self['nodePath']]) def undoHook(self, nodePathList = []): # Reflect new changes @@ -744,7 +745,7 @@ class Placer(AppShell): self.undoButton.configure(state = 'disabled') def pushRedo(self): - base.direct.pushRedo([self['nodePath']]) + ShowBaseGlobal.direct.pushRedo([self['nodePath']]) def redoHook(self, nodePathList = []): # Reflect new changes diff --git a/direct/src/wxwidgets/WxPandaShell.py b/direct/src/wxwidgets/WxPandaShell.py index 44790bd653..09e15add96 100755 --- a/direct/src/wxwidgets/WxPandaShell.py +++ b/direct/src/wxwidgets/WxPandaShell.py @@ -9,6 +9,8 @@ from direct.task.TaskManagerGlobal import taskMgr from .WxAppShell import WxAppShell from .ViewPort import Viewport, ViewportManager +from typing import Optional + ID_FOUR_VIEW = 401 ID_TOP_VIEW = 402 ID_FRONT_VIEW = 403 @@ -25,7 +27,7 @@ class WxPandaShell(WxAppShell): copyright = ('Copyright 2010 Disney Online Studios.' + '\nAll Rights Reserved.') - MENU_TEXTS = { + MENU_TEXTS: dict[int, tuple[str, Optional[str]]] = { ID_FOUR_VIEW: ("Four Views", None), ID_TOP_VIEW: ("Top View", None), ID_FRONT_VIEW: ("Front View", None), @@ -114,6 +116,7 @@ class WxPandaShell(WxAppShell): self.wxStep() ViewportManager.initializeAll() # Position the camera + base = ShowBaseGlobal.base if base.trackball is not None: base.trackball.node().setPos(0, 30, 0) base.trackball.node().setHpr(0, 15, 0) @@ -125,33 +128,34 @@ class WxPandaShell(WxAppShell): # initializing direct if self.fStartDirect: base.startDirect(fWantTk = 0, fWantWx = 0) + direct = ShowBaseGlobal.direct - base.direct.disableMouseEvents() - newMouseEvents = ["_le_per_%s"%x for x in base.direct.mouseEvents] +\ - ["_le_fro_%s"%x for x in base.direct.mouseEvents] +\ - ["_le_lef_%s"%x for x in base.direct.mouseEvents] +\ - ["_le_top_%s"%x for x in base.direct.mouseEvents] - base.direct.mouseEvents = newMouseEvents - base.direct.enableMouseEvents() + direct.disableMouseEvents() + newMouseEvents = ["_le_per_%s"%x for x in direct.mouseEvents] +\ + ["_le_fro_%s"%x for x in direct.mouseEvents] +\ + ["_le_lef_%s"%x for x in direct.mouseEvents] +\ + ["_le_top_%s"%x for x in direct.mouseEvents] + direct.mouseEvents = newMouseEvents + direct.enableMouseEvents() - base.direct.disableKeyEvents() - keyEvents = ["_le_per_%s"%x for x in base.direct.keyEvents] +\ - ["_le_fro_%s"%x for x in base.direct.keyEvents] +\ - ["_le_lef_%s"%x for x in base.direct.keyEvents] +\ - ["_le_top_%s"%x for x in base.direct.keyEvents] - base.direct.keyEvents = keyEvents - base.direct.enableKeyEvents() + direct.disableKeyEvents() + keyEvents = ["_le_per_%s"%x for x in direct.keyEvents] +\ + ["_le_fro_%s"%x for x in direct.keyEvents] +\ + ["_le_lef_%s"%x for x in direct.keyEvents] +\ + ["_le_top_%s"%x for x in direct.keyEvents] + direct.keyEvents = keyEvents + direct.enableKeyEvents() - base.direct.disableModifierEvents() - modifierEvents = ["_le_per_%s"%x for x in base.direct.modifierEvents] +\ - ["_le_fro_%s"%x for x in base.direct.modifierEvents] +\ - ["_le_lef_%s"%x for x in base.direct.modifierEvents] +\ - ["_le_top_%s"%x for x in base.direct.modifierEvents] - base.direct.modifierEvents = modifierEvents - base.direct.enableModifierEvents() + direct.disableModifierEvents() + modifierEvents = ["_le_per_%s"%x for x in direct.modifierEvents] +\ + ["_le_fro_%s"%x for x in direct.modifierEvents] +\ + ["_le_lef_%s"%x for x in direct.modifierEvents] +\ + ["_le_top_%s"%x for x in direct.modifierEvents] + direct.modifierEvents = modifierEvents + direct.enableModifierEvents() - base.direct.cameraControl.lockRoll = True - base.direct.setFScaleWidgetByCam(1) + direct.cameraControl.lockRoll = True + direct.setFScaleWidgetByCam(1) unpickables = [ "z-guide", @@ -172,31 +176,31 @@ class WxPandaShell(WxAppShell): "Sphere",] for unpickable in unpickables: - base.direct.addUnpickable(unpickable) + direct.addUnpickable(unpickable) - base.direct.manipulationControl.optionalSkipFlags |= SKIP_UNPICKABLE - base.direct.manipulationControl.fAllowMarquee = 1 - base.direct.manipulationControl.supportMultiView() - base.direct.cameraControl.useMayaCamControls = 1 - base.direct.cameraControl.perspCollPlane = self.perspView.collPlane - base.direct.cameraControl.perspCollPlane2 = self.perspView.collPlane2 + direct.manipulationControl.optionalSkipFlags |= SKIP_UNPICKABLE + direct.manipulationControl.fAllowMarquee = 1 + direct.manipulationControl.supportMultiView() + direct.cameraControl.useMayaCamControls = 1 + direct.cameraControl.perspCollPlane = self.perspView.collPlane + direct.cameraControl.perspCollPlane2 = self.perspView.collPlane2 - for widget in base.direct.manipulationControl.widgetList: + for widget in direct.manipulationControl.widgetList: widget.setBin('gui-popup', 0) widget.setDepthTest(0) # [gjeon] to intercept messages here - base.direct.ignore('DIRECT-delete') - base.direct.ignore('DIRECT-select') - base.direct.ignore('DIRECT-preDeselectAll') - base.direct.ignore('DIRECT-toggleWidgetVis') - base.direct.fIgnoreDirectOnlyKeyMap = 1 + direct.ignore('DIRECT-delete') + direct.ignore('DIRECT-select') + direct.ignore('DIRECT-preDeselectAll') + direct.ignore('DIRECT-toggleWidgetVis') + direct.fIgnoreDirectOnlyKeyMap = 1 # [gjeon] do not use the old way of finding current DR - base.direct.drList.tryToGetCurrentDr = False + direct.drList.tryToGetCurrentDr = False else: - base.direct=None + base.direct = None #base.closeWindow(base.win) base.win = base.winList[3] From 3922800aa45268de39a3936124cc4a8a3f0772be Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 11 Oct 2023 10:26:56 +0200 Subject: [PATCH 13/29] tkpanels: Fix error running with Python 3.8 --- direct/src/tkpanels/Inspector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/direct/src/tkpanels/Inspector.py b/direct/src/tkpanels/Inspector.py index 707bc038c3..29bebcf16f 100644 --- a/direct/src/tkpanels/Inspector.py +++ b/direct/src/tkpanels/Inspector.py @@ -31,7 +31,7 @@ def inspect(anObject): ### private -_InspectorMap: dict[str, str] +_InspectorMap: "dict[str, str]" def inspectorFor(anObject): From c00f3b18f54c5f294a403f79f6f6d841d8298c25 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 11 Oct 2023 11:07:56 +0200 Subject: [PATCH 14/29] direct: More reduction of reliance on builtins, mypy fixes --- direct/src/directtools/DirectCameraControl.py | 353 ++++++++++-------- direct/src/directtools/DirectGrid.py | 7 +- direct/src/directtools/DirectManipulation.py | 2 +- .../extensions_native/NodePath_extensions.py | 16 +- direct/src/showbase/ShowBase.py | 1 + direct/src/showbase/ShowBaseGlobal.py | 3 + direct/src/showbase/TkGlobal.py | 3 +- direct/src/showbase/WxGlobal.py | 3 +- direct/src/task/MiniTask.py | 2 + direct/src/wxwidgets/WxAppShell.py | 9 +- 10 files changed, 218 insertions(+), 181 deletions(-) diff --git a/direct/src/directtools/DirectCameraControl.py b/direct/src/directtools/DirectCameraControl.py index 59db9bf185..96ddb89d53 100644 --- a/direct/src/directtools/DirectCameraControl.py +++ b/direct/src/directtools/DirectCameraControl.py @@ -1,6 +1,7 @@ import math from panda3d.core import BitMask32, Mat4, NodePath, Point3, VBase3, Vec3, Vec4, rad2Deg from direct.showbase.DirectObject import DirectObject +from direct.showbase import ShowBaseGlobal from .DirectUtil import CLAMP, useDirectRenderStyle from .DirectGeometry import getCrankAngle, getScreenXY from . import DirectGlobals as DG @@ -26,7 +27,7 @@ class DirectCameraControl(DirectObject): self.orthoViewRoll = 0.0 self.lastView = 0 self.coa = Point3(0, 100, 0) - self.coaMarker = base.loader.loadModel('models/misc/sphere') + self.coaMarker = ShowBaseGlobal.loader.loadModel('models/misc/sphere') self.coaMarker.setName('DirectCameraCOAMarker') self.coaMarker.setTransparency(1) self.coaMarker.setColor(1, 0, 0, 0) @@ -37,8 +38,8 @@ class DirectCameraControl(DirectObject): self.fLockCOA = 0 self.nullHitPointCount = 0 self.cqEntries = [] - self.coaMarkerRef = base.direct.group.attachNewNode('coaMarkerRef') - self.camManipRef = base.direct.group.attachNewNode('camManipRef') + self.coaMarkerRef = ShowBaseGlobal.direct.group.attachNewNode('coaMarkerRef') + self.camManipRef = ShowBaseGlobal.direct.group.attachNewNode('camManipRef') self.switchDirBelowZero = True self.manipulateCameraTask = None self.manipulateCameraInterval = None @@ -112,11 +113,6 @@ class DirectCameraControl(DirectObject): self.perspCollPlane2 = None # [gjeon] used for new LE def toggleMarkerVis(self): -## if base.direct.cameraControl.coaMarker.isHidden(): -## base.direct.cameraControl.coaMarker.show() -## else: -## base.direct.cameraControl.coaMarker.hide() - if self.coaMarker.isHidden(): self.coaMarker.show() else: @@ -132,11 +128,14 @@ class DirectCameraControl(DirectObject): # Hide the marker for this kind of motion self.coaMarker.hide() # Record time of start of mouse interaction + base = ShowBaseGlobal.base self.startT = base.clock.getFrameTime() self.startF = base.clock.getFrameCount() # If the cam is orthogonal, spawn differentTask - if hasattr(base.direct, "manipulationControl") and base.direct.manipulationControl.fMultiView and\ - base.direct.camera.getName() != 'persp': + direct = ShowBaseGlobal.direct + if hasattr(direct, "manipulationControl") and \ + direct.manipulationControl.fMultiView and \ + direct.camera.getName() != 'persp': self.spawnOrthoZoom() else: # Start manipulation @@ -167,7 +166,9 @@ class DirectCameraControl(DirectObject): def mouseFlyStart(self, modifiers): # Record undo point - # base.direct.pushUndo([base.direct.camera]) # Wasteful use of undo + base = ShowBaseGlobal.base + direct = ShowBaseGlobal.direct + #direct.pushUndo([direct.camera]) # Wasteful use of undo if self.useMayaCamControls and modifiers == 4: # alt is down, use maya controls # Hide the marker for this kind of motion self.coaMarker.hide() @@ -176,15 +177,16 @@ class DirectCameraControl(DirectObject): self.startF = base.clock.getFrameCount() # Start manipulation # If the cam is orthogonal, spawn differentTask - if hasattr(base.direct, "manipulationControl") and base.direct.manipulationControl.fMultiView and\ - base.direct.camera.getName() != 'persp': + if hasattr(direct, "manipulationControl") and \ + direct.manipulationControl.fMultiView and \ + direct.camera.getName() != 'persp': self.spawnOrthoTranslate() else: self.spawnXZTranslate() self.altDown = 1 elif not self.useMayaCamControls: # Where are we in the display region? - if ((abs(base.direct.dr.mouseX) < 0.9) and (abs(base.direct.dr.mouseY) < 0.9)): + if abs(direct.dr.mouseX) < 0.9 and abs(direct.dr.mouseY) < 0.9: # MOUSE IS IN CENTRAL REGION # Hide the marker for this kind of motion self.coaMarker.hide() @@ -194,19 +196,18 @@ class DirectCameraControl(DirectObject): # Start manipulation self.spawnXZTranslateOrHPanYZoom() # END MOUSE IN CENTRAL REGION + elif abs(direct.dr.mouseX) > 0.9 and abs(direct.dr.mouseY) > 0.9: + # Mouse is in corners, spawn roll task + self.spawnMouseRollTask() else: - if ((abs(base.direct.dr.mouseX) > 0.9) and - (abs(base.direct.dr.mouseY) > 0.9)): - # Mouse is in corners, spawn roll task - self.spawnMouseRollTask() - else: - # Mouse is in outer frame, spawn mouseRotateTask - self.spawnMouseRotateTask() + # Mouse is in outer frame, spawn mouseRotateTask + self.spawnMouseRotateTask() if not modifiers == 4: self.altDown = 0 def mouseFlyStop(self): self.__stopManipulateCamera() + base = ShowBaseGlobal.base stopT = base.clock.getFrameTime() deltaT = stopT - self.startT stopF = base.clock.getFrameCount() @@ -215,7 +216,8 @@ class DirectCameraControl(DirectObject): # if not self.useMayaCamControls and (deltaT <= 0.25) or (deltaF <= 1): # Do this when not trying to manipulate camera - if not self.altDown and len(base.direct.selected.getSelectedAsList()) == 0: + direct = ShowBaseGlobal.direct + if not self.altDown and len(direct.selected.getSelectedAsList()) == 0: # Check for a hit point based on # current mouse position # Allow intersection with unpickable objects @@ -224,13 +226,13 @@ class DirectCameraControl(DirectObject): skipFlags = DG.SKIP_HIDDEN | DG.SKIP_BACKFACE # Skip camera (and its children), unless control key is pressed skipFlags |= DG.SKIP_CAMERA * (1 - base.getControl()) - self.computeCOA(base.direct.iRay.pickGeom(skipFlags = skipFlags)) + self.computeCOA(direct.iRay.pickGeom(skipFlags = skipFlags)) # Record reference point self.coaMarkerRef.setPosHprScale(base.cam, 0, 0, 0, 0, 0, 0, 1, 1, 1) # Record entries self.cqEntries = [] - for i in range(base.direct.iRay.getNumEntries()): - self.cqEntries.append(base.direct.iRay.getEntry(i)) + for i in range(direct.iRay.getNumEntries()): + self.cqEntries.append(direct.iRay.getEntry(i)) # Show the marker self.coaMarker.show() # Resize it @@ -251,7 +253,7 @@ class DirectCameraControl(DirectObject): # Spawn the new task t = Task.Task(self.XZTranslateOrHPanYZoomTask) # For HPanYZoom - t.zoomSF = Vec3(self.coaMarker.getPos(base.direct.camera)).length() + t.zoomSF = Vec3(self.coaMarker.getPos(ShowBaseGlobal.direct.camera)).length() self.__startManipulateCamera(task = t) def spawnXZTranslateOrHPPan(self): @@ -277,7 +279,7 @@ class DirectCameraControl(DirectObject): self.__stopManipulateCamera() # Spawn new task t = Task.Task(self.HPanYZoomTask) - t.zoomSF = Vec3(self.coaMarker.getPos(base.direct.camera)).length() + t.zoomSF = Vec3(self.coaMarker.getPos(ShowBaseGlobal.direct.camera)).length() self.__startManipulateCamera(task = t) def spawnOrthoZoom(self): @@ -294,13 +296,13 @@ class DirectCameraControl(DirectObject): self.__startManipulateCamera(func = self.HPPanTask) def XZTranslateOrHPanYZoomTask(self, state): - if base.direct.fShift: + if ShowBaseGlobal.direct.fShift: return self.XZTranslateTask(state) else: return self.HPanYZoomTask(state) def XZTranslateOrHPPanTask(self, state): - if base.direct.fShift: + if ShowBaseGlobal.direct.fShift: # Panning action return self.HPPanTask(state) else: @@ -308,43 +310,46 @@ class DirectCameraControl(DirectObject): return self.XZTranslateTask(state) def XZTranslateTask(self, state): - coaDist = Vec3(self.coaMarker.getPos(base.direct.camera)).length() - xlateSF = coaDist / base.direct.dr.near - base.direct.camera.setPos(base.direct.camera, - (-0.5 * base.direct.dr.mouseDeltaX * - base.direct.dr.nearWidth * + direct = ShowBaseGlobal.direct + coaDist = Vec3(self.coaMarker.getPos(direct.camera)).length() + xlateSF = coaDist / direct.dr.near + direct.camera.setPos(direct.camera, + (-0.5 * direct.dr.mouseDeltaX * + direct.dr.nearWidth * xlateSF), 0.0, - (-0.5 * base.direct.dr.mouseDeltaY * - base.direct.dr.nearHeight * + (-0.5 * direct.dr.mouseDeltaY * + direct.dr.nearHeight * xlateSF)) return Task.cont def OrthoTranslateTask(self, state): # create ray from the camera to detect 3d position - iRay = SelectionRay(base.direct.camera) - iRay.collider.setFromLens(base.direct.camNode, base.direct.dr.mouseX, base.direct.dr.mouseY) + direct = ShowBaseGlobal.direct + iRay = SelectionRay(direct.camera) + iRay.collider.setFromLens(direct.camNode, direct.dr.mouseX, direct.dr.mouseY) #iRay.collideWithBitMask(1) iRay.collideWithBitMask(BitMask32.bit(21)) - iRay.ct.traverse(base.direct.grid) + iRay.ct.traverse(direct.grid) entry = iRay.getEntry(0) hitPt = entry.getSurfacePoint(entry.getFromNodePath()) iRay.collisionNodePath.removeNode() del iRay if hasattr(state, 'prevPt'): - base.direct.camera.setPos(base.direct.camera, (state.prevPt - hitPt)) + direct.camera.setPos(direct.camera, (state.prevPt - hitPt)) state.prevPt = hitPt return Task.cont def HPanYZoomTask(self, state): # If the cam is orthogonal, don't rotate or zoom. - if (hasattr(base.direct.cam.node(), "getLens") and - base.direct.cam.node().getLens().__class__.__name__ == "OrthographicLens"): + direct = ShowBaseGlobal.direct + if (hasattr(direct.cam.node(), "getLens") and + direct.cam.node().getLens().__class__.__name__ == "OrthographicLens"): return - if base.direct.fControl: - moveDir = Vec3(self.coaMarker.getPos(base.direct.camera)) + if direct.fControl: + moveDir = Vec3(self.coaMarker.getPos(direct.camera)) # If marker is behind camera invert vector if moveDir[1] < 0.0: moveDir.assign(moveDir * -1) @@ -353,18 +358,18 @@ class DirectCameraControl(DirectObject): moveDir = Vec3(Y_AXIS) if self.useMayaCamControls: # use maya controls - moveDir.assign(moveDir * ((base.direct.dr.mouseDeltaX -1.0 * base.direct.dr.mouseDeltaY) + moveDir.assign(moveDir * ((direct.dr.mouseDeltaX -1.0 * direct.dr.mouseDeltaY) * state.zoomSF)) hVal = 0.0 else: - moveDir.assign(moveDir * (-1.0 * base.direct.dr.mouseDeltaY * + moveDir.assign(moveDir * (-1.0 * direct.dr.mouseDeltaY * state.zoomSF)) - if base.direct.dr.mouseDeltaY > 0.0: + if direct.dr.mouseDeltaY > 0.0: moveDir.setY(moveDir[1] * 1.0) - hVal = 0.5 * base.direct.dr.mouseDeltaX * base.direct.dr.fovH + hVal = 0.5 * direct.dr.mouseDeltaX * direct.dr.fovH - base.direct.camera.setPosHpr(base.direct.camera, + direct.camera.setPosHpr(direct.camera, moveDir[0], moveDir[1], moveDir[2], @@ -372,39 +377,42 @@ class DirectCameraControl(DirectObject): 0.0, 0.0) if self.lockRoll: # flatten roll - base.direct.camera.setR(0) + direct.camera.setR(0) return Task.cont def OrthoZoomTask(self, state): - filmSize = base.direct.camNode.getLens().getFilmSize() - factor = (base.direct.dr.mouseDeltaX -1.0 * base.direct.dr.mouseDeltaY) * 0.1 - x = base.direct.dr.getWidth() - y = base.direct.dr.getHeight() - base.direct.dr.orthoFactor -= factor - if base.direct.dr.orthoFactor < 0: - base.direct.dr.orthoFactor = 0.0001 - base.direct.dr.updateFilmSize(x, y) + direct = ShowBaseGlobal.direct + filmSize = direct.camNode.getLens().getFilmSize() + factor = (direct.dr.mouseDeltaX -1.0 * direct.dr.mouseDeltaY) * 0.1 + x = direct.dr.getWidth() + y = direct.dr.getHeight() + direct.dr.orthoFactor -= factor + if direct.dr.orthoFactor < 0: + direct.dr.orthoFactor = 0.0001 + direct.dr.updateFilmSize(x, y) return Task.cont def HPPanTask(self, state): - base.direct.camera.setHpr(base.direct.camera, - (0.5 * base.direct.dr.mouseDeltaX * - base.direct.dr.fovH), - (-0.5 * base.direct.dr.mouseDeltaY * - base.direct.dr.fovV), + direct = ShowBaseGlobal.direct + direct.camera.setHpr(direct.camera, + (0.5 * direct.dr.mouseDeltaX * + direct.dr.fovH), + (-0.5 * direct.dr.mouseDeltaY * + direct.dr.fovV), 0.0) return Task.cont def spawnMouseRotateTask(self): # Kill any existing tasks self.__stopManipulateCamera() + direct = ShowBaseGlobal.direct if self.perspCollPlane: - iRay = SelectionRay(base.direct.camera) - iRay.collider.setFromLens(base.direct.camNode, 0.0, 0.0) + iRay = SelectionRay(direct.camera) + iRay.collider.setFromLens(direct.camNode, 0.0, 0.0) iRay.collideWithBitMask(1) - if base.direct.camera.getPos().getZ() >=0: + if direct.camera.getPos().getZ() >=0: iRay.ct.traverse(self.perspCollPlane) else: iRay.ct.traverse(self.perspCollPlane2) @@ -415,7 +423,7 @@ class DirectCameraControl(DirectObject): # create a temp nodePath to get the position np = NodePath('temp') - np.setPos(base.direct.camera, hitPt) + np.setPos(direct.camera, hitPt) self.coaMarkerPos = np.getPos() np.removeNode() self.coaMarker.setPos(self.coaMarkerPos) @@ -425,9 +433,9 @@ class DirectCameraControl(DirectObject): # Set at markers position in render coordinates self.camManipRef.setPos(self.coaMarkerPos) - self.camManipRef.setHpr(base.direct.camera, DG.ZERO_POINT) + self.camManipRef.setHpr(direct.camera, DG.ZERO_POINT) t = Task.Task(self.mouseRotateTask) - if abs(base.direct.dr.mouseX) > 0.9: + if abs(direct.dr.mouseX) > 0.9: t.constrainedDir = 'y' else: t.constrainedDir = 'x' @@ -435,36 +443,37 @@ class DirectCameraControl(DirectObject): def mouseRotateTask(self, state): # If the cam is orthogonal, don't rotate. - if (hasattr(base.direct.cam.node(), "getLens") and - base.direct.cam.node().getLens().__class__.__name__ == "OrthographicLens"): + direct = ShowBaseGlobal.direct + if (hasattr(direct.cam.node(), "getLens") and + direct.cam.node().getLens().__class__.__name__ == "OrthographicLens"): return # If moving outside of center, ignore motion perpendicular to edge - if ((state.constrainedDir == 'y') and (abs(base.direct.dr.mouseX) > 0.9)): + if ((state.constrainedDir == 'y') and (abs(direct.dr.mouseX) > 0.9)): deltaX = 0 - deltaY = base.direct.dr.mouseDeltaY - elif ((state.constrainedDir == 'x') and (abs(base.direct.dr.mouseY) > 0.9)): - deltaX = base.direct.dr.mouseDeltaX + deltaY = direct.dr.mouseDeltaY + elif ((state.constrainedDir == 'x') and (abs(direct.dr.mouseY) > 0.9)): + deltaX = direct.dr.mouseDeltaX deltaY = 0 else: - deltaX = base.direct.dr.mouseDeltaX - deltaY = base.direct.dr.mouseDeltaY - if base.direct.fShift: - base.direct.camera.setHpr(base.direct.camera, - (deltaX * base.direct.dr.fovH), - (-deltaY * base.direct.dr.fovV), + deltaX = direct.dr.mouseDeltaX + deltaY = direct.dr.mouseDeltaY + if direct.fShift: + direct.camera.setHpr(direct.camera, + (deltaX * direct.dr.fovH), + (-deltaY * direct.dr.fovV), 0.0) if self.lockRoll: # flatten roll - base.direct.camera.setR(0) + direct.camera.setR(0) self.camManipRef.setPos(self.coaMarkerPos) - self.camManipRef.setHpr(base.direct.camera, DG.ZERO_POINT) + self.camManipRef.setHpr(direct.camera, DG.ZERO_POINT) else: - if base.direct.camera.getPos().getZ() >=0 or not self.switchDirBelowZero: + if direct.camera.getPos().getZ() >=0 or not self.switchDirBelowZero: dirX = -1 else: dirX = 1 - wrt = base.direct.camera.getTransform(self.camManipRef) + wrt = direct.camera.getTransform(self.camManipRef) self.camManipRef.setHpr(self.camManipRef, (dirX * deltaX * 180.0), (deltaY * 180.0), @@ -473,20 +482,21 @@ class DirectCameraControl(DirectObject): if self.lockRoll: # flatten roll self.camManipRef.setR(0) - base.direct.camera.setTransform(self.camManipRef, wrt) + direct.camera.setTransform(self.camManipRef, wrt) return Task.cont def spawnMouseRollTask(self): # Kill any existing tasks self.__stopManipulateCamera() # Set at markers position in render coordinates + direct = ShowBaseGlobal.direct self.camManipRef.setPos(self.coaMarkerPos) - self.camManipRef.setHpr(base.direct.camera, DG.ZERO_POINT) + self.camManipRef.setHpr(direct.camera, DG.ZERO_POINT) t = Task.Task(self.mouseRollTask) t.coaCenter = getScreenXY(self.coaMarker) t.lastAngle = getCrankAngle(t.coaCenter) # Store the camera/manipRef offset transform - t.wrt = base.direct.camera.getTransform(self.camManipRef) + t.wrt = direct.camera.getTransform(self.camManipRef) self.__startManipulateCamera(task = t) def mouseRollTask(self, state): @@ -498,23 +508,23 @@ class DirectCameraControl(DirectObject): if self.lockRoll: # flatten roll self.camManipRef.setR(0) - base.direct.camera.setTransform(self.camManipRef, wrt) + ShowBaseGlobal.direct.camera.setTransform(self.camManipRef, wrt) return Task.cont def lockCOA(self): self.fLockCOA = 1 - base.direct.message('COA Lock On') + ShowBaseGlobal.direct.message('COA Lock On') def unlockCOA(self): self.fLockCOA = 0 - base.direct.message('COA Lock Off') + ShowBaseGlobal.direct.message('COA Lock Off') def toggleCOALock(self): self.fLockCOA = 1 - self.fLockCOA if self.fLockCOA: - base.direct.message('COA Lock On') + ShowBaseGlobal.direct.message('COA Lock On') else: - base.direct.message('COA Lock Off') + ShowBaseGlobal.direct.message('COA Lock Off') def pickNextCOA(self): """ Cycle through collision handler entries """ @@ -524,7 +534,7 @@ class DirectCameraControl(DirectObject): self.cqEntries = self.cqEntries[1:] + self.cqEntries[:1] # Filter out object's under camera nodePath = entry.getIntoNodePath() - if base.direct.camera not in nodePath.getAncestors(): + if ShowBaseGlobal.direct.camera not in nodePath.getAncestors(): # Compute new hit point hitPt = entry.getSurfacePoint(entry.getFromNodePath()) # Move coa marker to new point @@ -536,11 +546,11 @@ class DirectCameraControl(DirectObject): def computeCOA(self, entry): coa = Point3(0) - dr = base.direct.drList.getCurrentDr() + dr = ShowBaseGlobal.direct.drList.getCurrentDr() if self.fLockCOA: # COA is locked, use existing point # Use existing point - coa.assign(self.coaMarker.getPos(base.direct.camera)) + coa.assign(self.coaMarker.getPos(ShowBaseGlobal.direct.camera)) # Reset hit point count self.nullHitPointCount = 0 elif entry: @@ -553,7 +563,7 @@ class DirectCameraControl(DirectObject): if ((hitPtDist < (1.1 * dr.near)) or (hitPtDist > dr.far)): # Just use existing point - coa.assign(self.coaMarker.getPos(base.direct.camera)) + coa.assign(self.coaMarker.getPos(ShowBaseGlobal.direct.camera)) # Reset hit point count self.nullHitPointCount = 0 else: @@ -565,7 +575,7 @@ class DirectCameraControl(DirectObject): # MRM: Would be nice to be able to control this # At least display it dist = pow(10.0, self.nullHitPointCount) - base.direct.message('COA Distance: ' + repr(dist)) + ShowBaseGlobal.direct.message('COA Distance: ' + repr(dist)) coa.set(0, dist, 0) # Compute COA Dist coaDist = Vec3(coa - DG.ZERO_POINT).length() @@ -583,7 +593,7 @@ class DirectCameraControl(DirectObject): if ref is None: # KEH: use the current display region # ref = base.cam - ref = base.direct.drList.getCurrentDr().cam + ref = ShowBaseGlobal.direct.drList.getCurrentDr().cam self.coaMarker.setPos(ref, self.coa) pos = self.coaMarker.getPos() self.coaMarker.setPosHprScale(pos, Vec3(0), Vec3(1)) @@ -598,10 +608,10 @@ class DirectCameraControl(DirectObject): def updateCoaMarkerSize(self, coaDist = None): if not coaDist: - coaDist = Vec3(self.coaMarker.getPos(base.direct.camera)).length() + coaDist = Vec3(self.coaMarker.getPos(ShowBaseGlobal.direct.camera)).length() # Nominal size based on default 30 degree vertical FOV # Need to adjust size based on distance and current FOV - sf = COA_MARKER_SF * coaDist * (base.direct.drList.getCurrentDr().fovV/30.0) + sf = COA_MARKER_SF * coaDist * (ShowBaseGlobal.direct.drList.getCurrentDr().fovV/30.0) if sf == 0.0: sf = 0.1 self.coaMarker.setScale(sf) @@ -619,32 +629,36 @@ class DirectCameraControl(DirectObject): def homeCam(self): # Record undo point - base.direct.pushUndo([base.direct.camera]) - base.direct.camera.reparentTo(render) - base.direct.camera.clearMat() + direct = ShowBaseGlobal.direct + direct.pushUndo([direct.camera]) + direct.camera.reparentTo(ShowBaseGlobal.base.render) + direct.camera.clearMat() # Resize coa marker self.updateCoaMarkerSize() def uprightCam(self): self.__stopManipulateCamera() # Record undo point - base.direct.pushUndo([base.direct.camera]) + direct = ShowBaseGlobal.direct + direct.pushUndo([direct.camera]) # Pitch camera till upright - currH = base.direct.camera.getH() - ival = base.direct.camera.hprInterval(CAM_MOVE_DURATION, - (currH, 0, 0), - other = render, - blendType = 'easeInOut', - name = 'manipulateCamera') - self.__startManipulateCamera(ival = ival) + currH = direct.camera.getH() + ival = direct.camera.hprInterval(CAM_MOVE_DURATION, + (currH, 0, 0), + other=ShowBaseGlobal.base.render, + blendType='easeInOut', + name='manipulateCamera') + self.__startManipulateCamera(ival=ival) def orbitUprightCam(self): self.__stopManipulateCamera() # Record undo point - base.direct.pushUndo([base.direct.camera]) + direct = ShowBaseGlobal.direct + direct.pushUndo([direct.camera]) # Transform camera z axis to render space + render = ShowBaseGlobal.base.render mCam2Render = Mat4(Mat4.identMat()) # [gjeon] fixed to give required argument - mCam2Render.assign(base.direct.camera.getMat(render)) + mCam2Render.assign(direct.camera.getMat(render)) zAxis = Vec3(mCam2Render.xformVec(DG.Z_AXIS)) zAxis.normalize() # Compute rotation angle needed to upright cam @@ -665,8 +679,8 @@ class DirectCameraControl(DirectObject): self.camManipRef.setPos(self.coaMarker, Vec3(0)) self.camManipRef.setHpr(render, rotAngle, 0, 0) # Reparent Cam to ref Coordinate system - parent = base.direct.camera.getParent() - base.direct.camera.wrtReparentTo(self.camManipRef) + parent = direct.camera.getParent() + direct.camera.wrtReparentTo(self.camManipRef) # Rotate ref CS to final orientation ival = self.camManipRef.hprInterval(CAM_MOVE_DURATION, (rotAngle, orbitAngle, 0), @@ -685,17 +699,18 @@ class DirectCameraControl(DirectObject): def centerCamIn(self, t): self.__stopManipulateCamera() # Record undo point - base.direct.pushUndo([base.direct.camera]) + direct = ShowBaseGlobal.direct + direct.pushUndo([direct.camera]) # Determine marker location - markerToCam = self.coaMarker.getPos(base.direct.camera) + markerToCam = self.coaMarker.getPos(direct.camera) dist = Vec3(markerToCam - DG.ZERO_POINT).length() scaledCenterVec = Y_AXIS * dist delta = markerToCam - scaledCenterVec - self.camManipRef.setPosHpr(base.direct.camera, Point3(0), Point3(0)) - ival = base.direct.camera.posInterval(CAM_MOVE_DURATION, - Point3(delta), - other = self.camManipRef, - blendType = 'easeInOut') + self.camManipRef.setPosHpr(direct.camera, Point3(0), Point3(0)) + ival = direct.camera.posInterval(CAM_MOVE_DURATION, + Point3(delta), + other=self.camManipRef, + blendType='easeInOut') ival = Sequence(ival, Func(self.updateCoaMarkerSizeOnDeath), name = 'manipulateCamera') self.__startManipulateCamera(ival = ival) @@ -703,17 +718,18 @@ class DirectCameraControl(DirectObject): def zoomCam(self, zoomFactor, t): self.__stopManipulateCamera() # Record undo point - base.direct.pushUndo([base.direct.camera]) + direct = ShowBaseGlobal.direct + direct.pushUndo([direct.camera]) # Find a point zoom factor times the current separation # of the widget and cam - zoomPtToCam = self.coaMarker.getPos(base.direct.camera) * zoomFactor + zoomPtToCam = self.coaMarker.getPos(direct.camera) * zoomFactor # Put a target nodePath there - self.camManipRef.setPos(base.direct.camera, zoomPtToCam) + self.camManipRef.setPos(direct.camera, zoomPtToCam) # Move to that point - ival = base.direct.camera.posInterval(CAM_MOVE_DURATION, - DG.ZERO_POINT, - other = self.camManipRef, - blendType = 'easeInOut') + ival = direct.camera.posInterval(CAM_MOVE_DURATION, + DG.ZERO_POINT, + other=self.camManipRef, + blendType='easeInOut') ival = Sequence(ival, Func(self.updateCoaMarkerSizeOnDeath), name = 'manipulateCamera') self.__startManipulateCamera(ival = ival) @@ -722,7 +738,8 @@ class DirectCameraControl(DirectObject): # Kill any existing tasks self.__stopManipulateCamera() # Record undo point - base.direct.pushUndo([base.direct.camera]) + direct = ShowBaseGlobal.direct + direct.pushUndo([direct.camera]) # Calc hprOffset hprOffset = VBase3() if view == 8: @@ -751,7 +768,7 @@ class DirectCameraControl(DirectObject): self.camManipRef.setPosHpr(self.coaMarker, DG.ZERO_VEC, hprOffset) # Scale center vec by current distance to target - offsetDistance = Vec3(base.direct.camera.getPos(self.camManipRef) - + offsetDistance = Vec3(direct.camera.getPos(self.camManipRef) - DG.ZERO_POINT).length() scaledCenterVec = Y_AXIS * (-1.0 * offsetDistance) # Now put the camManipRef at that point @@ -760,11 +777,11 @@ class DirectCameraControl(DirectObject): DG.ZERO_VEC) # Record view for next time around self.lastView = view - ival = base.direct.camera.posHprInterval(CAM_MOVE_DURATION, - pos = DG.ZERO_POINT, - hpr = VBase3(0, 0, self.orthoViewRoll), - other = self.camManipRef, - blendType = 'easeInOut') + ival = direct.camera.posHprInterval(CAM_MOVE_DURATION, + pos=DG.ZERO_POINT, + hpr=VBase3(0, 0, self.orthoViewRoll), + other=self.camManipRef, + blendType='easeInOut') ival = Sequence(ival, Func(self.updateCoaMarkerSizeOnDeath), name = 'manipulateCamera') self.__startManipulateCamera(ival = ival) @@ -774,15 +791,16 @@ class DirectCameraControl(DirectObject): self.__stopManipulateCamera() # Record undo point - base.direct.pushUndo([base.direct.camera]) + direct = ShowBaseGlobal.direct + direct.pushUndo([direct.camera]) # Coincident with widget self.camManipRef.setPos(self.coaMarker, DG.ZERO_POINT) # But aligned with render space self.camManipRef.setHpr(DG.ZERO_POINT) - parent = base.direct.camera.getParent() - base.direct.camera.wrtReparentTo(self.camManipRef) + parent = direct.camera.getParent() + direct.camera.wrtReparentTo(self.camManipRef) ival = self.camManipRef.hprInterval(CAM_MOVE_DURATION, VBase3(degrees, 0, 0), @@ -792,7 +810,7 @@ class DirectCameraControl(DirectObject): self.__startManipulateCamera(ival = ival) def reparentCam(self, parent): - base.direct.camera.wrtReparentTo(parent) + ShowBaseGlobal.direct.camera.wrtReparentTo(parent) self.updateCoaMarkerSize() def fitOnWidget(self, nodePath = 'None Given'): @@ -800,75 +818,78 @@ class DirectCameraControl(DirectObject): # stop any ongoing tasks self.__stopManipulateCamera() # How big is the node? - nodeScale = base.direct.widget.scalingNode.getScale(render) + direct = ShowBaseGlobal.direct + nodeScale = direct.widget.scalingNode.getScale(ShowBaseGlobal.base.render) maxScale = max(nodeScale[0], nodeScale[1], nodeScale[2]) - maxDim = min(base.direct.dr.nearWidth, base.direct.dr.nearHeight) + maxDim = min(direct.dr.nearWidth, direct.dr.nearHeight) # At what distance does the object fill 30% of the screen? # Assuming radius of 1 on widget - camY = base.direct.dr.near * (2.0 * maxScale)/(0.3 * maxDim) + camY = direct.dr.near * (2.0 * maxScale) / (0.3 * maxDim) # What is the vector through the center of the screen? centerVec = Y_AXIS * camY # Where is the node relative to the viewpoint - vWidget2Camera = base.direct.widget.getPos(base.direct.camera) + vWidget2Camera = direct.widget.getPos(direct.camera) # How far do you move the camera to be this distance from the node? deltaMove = vWidget2Camera - centerVec # Move a target there try: - self.camManipRef.setPos(base.direct.camera, deltaMove) + self.camManipRef.setPos(direct.camera, deltaMove) except Exception: #self.notify.debug pass - parent = base.direct.camera.getParent() - base.direct.camera.wrtReparentTo(self.camManipRef) - ival = base.direct.camera.posInterval(CAM_MOVE_DURATION, - Point3(0, 0, 0), - blendType = 'easeInOut') + parent = direct.camera.getParent() + direct.camera.wrtReparentTo(self.camManipRef) + ival = direct.camera.posInterval(CAM_MOVE_DURATION, + Point3(0, 0, 0), + blendType='easeInOut') ival = Sequence(ival, Func(self.reparentCam, parent), - name = 'manipulateCamera') - self.__startManipulateCamera(ival = ival) + name='manipulateCamera') + self.__startManipulateCamera(ival=ival) def moveToFit(self): # How big is the active widget? - widgetScale = base.direct.widget.scalingNode.getScale(render) + direct = ShowBaseGlobal.direct + widgetScale = direct.widget.scalingNode.getScale(ShowBaseGlobal.base.render) maxScale = max(widgetScale[0], widgetScale[1], widgetScale[2]) # At what distance does the widget fill 50% of the screen? - camY = ((2 * base.direct.dr.near * (1.5 * maxScale)) / - min(base.direct.dr.nearWidth, base.direct.dr.nearHeight)) + camY = ((2 * direct.dr.near * (1.5 * maxScale)) / + min(direct.dr.nearWidth, direct.dr.nearHeight)) # Find a point this distance along the Y axis # MRM: This needs to be generalized to support non uniform frusta centerVec = Y_AXIS * camY # Before moving, record the relationship between the selected nodes # and the widget, so that this can be maintained - base.direct.selected.getWrtAll() + direct.selected.getWrtAll() # Push state onto undo stack - base.direct.pushUndo(base.direct.selected) + direct.pushUndo(direct.selected) # Remove the task to keep the widget attached to the object taskMgr.remove('followSelectedNodePath') # Spawn a task to keep the selected objects with the widget taskMgr.add(self.stickToWidgetTask, 'stickToWidget') # Spawn a task to move the widget - ival = base.direct.widget.posInterval(CAM_MOVE_DURATION, - Point3(centerVec), - other = base.direct.camera, - blendType = 'easeInOut') + ival = direct.widget.posInterval(CAM_MOVE_DURATION, + Point3(centerVec), + other=direct.camera, + blendType='easeInOut') ival = Sequence(ival, Func(lambda: taskMgr.remove('stickToWidget')), name = 'moveToFit') ival.start() def stickToWidgetTask(self, state): # Move the objects with the widget - base.direct.selected.moveWrtWidgetAll() + ShowBaseGlobal.direct.selected.moveWrtWidgetAll() # Continue return Task.cont def enableMouseFly(self, fKeyEvents = 1): # disable C++ fly interface + base = ShowBaseGlobal.base base.disableMouse() # Enable events for event in self.actionEvents: @@ -877,11 +898,11 @@ class DirectCameraControl(DirectObject): for event in self.keyEvents: self.accept(event[0], event[1], extraArgs = event[2:]) # Show marker - self.coaMarker.reparentTo(base.direct.group) + self.coaMarker.reparentTo(ShowBaseGlobal.direct.group) def disableMouseFly(self): # Hide the marker - self.coaMarker.reparentTo(hidden) + self.coaMarker.reparentTo(ShowBaseGlobal.hidden) # Ignore events for event in self.actionEvents: self.ignore(event[0]) @@ -890,7 +911,7 @@ class DirectCameraControl(DirectObject): # Kill tasks self.removeManipulateCameraTask() taskMgr.remove('stickToWidget') - base.enableMouse() + ShowBaseGlobal.base.enableMouse() def removeManipulateCameraTask(self): self.__stopManipulateCamera() diff --git a/direct/src/directtools/DirectGrid.py b/direct/src/directtools/DirectGrid.py index c8c7cb9693..33c6ac58dc 100644 --- a/direct/src/directtools/DirectGrid.py +++ b/direct/src/directtools/DirectGrid.py @@ -1,6 +1,7 @@ import math from panda3d.core import NodePath, Point3, VBase4 from direct.showbase.DirectObject import DirectObject +from direct.showbase import ShowBaseGlobal from .DirectUtil import ROUND_TO, useDirectRenderStyle from .DirectGeometry import LineNodePath @@ -14,7 +15,7 @@ class DirectGrid(NodePath, DirectObject): # Load up grid parts to initialize grid object # Polygon used to mark grid plane - self.gridBack = base.loader.loadModel('models/misc/gridBack') + self.gridBack = ShowBaseGlobal.loader.loadModel('models/misc/gridBack') self.gridBack.reparentTo(self) self.gridBack.setColor(*planeColor) @@ -36,7 +37,7 @@ class DirectGrid(NodePath, DirectObject): self.centerLines.setThickness(3) # Small marker to hilight snap-to-grid point - self.snapMarker = base.loader.loadModel('models/misc/sphere') + self.snapMarker = ShowBaseGlobal.loader.loadModel('models/misc/sphere') self.snapMarker.node().setName('gridSnapMarker') self.snapMarker.reparentTo(self) self.snapMarker.setColor(1, 0, 0, 1) @@ -55,7 +56,7 @@ class DirectGrid(NodePath, DirectObject): if parent: self.reparentTo(parent) else: - self.reparentTo(base.direct.group) + self.reparentTo(ShowBaseGlobal.direct.group) self.updateGrid() self.fEnabled = 1 diff --git a/direct/src/directtools/DirectManipulation.py b/direct/src/directtools/DirectManipulation.py index 98d48a9aa5..06b84c62a7 100644 --- a/direct/src/directtools/DirectManipulation.py +++ b/direct/src/directtools/DirectManipulation.py @@ -1113,7 +1113,7 @@ class ObjectHandles(NodePath, DirectObject): NodePath.__init__(self) # Load up object handles model and assign it to self - self.assign(ShowBaseGlobal.base.loader.loadModel('models/misc/objectHandles')) + self.assign(ShowBaseGlobal.loader.loadModel('models/misc/objectHandles')) self.setName(name) self.scalingNode = NodePath(self) self.scalingNode.setName('ohScalingNode') diff --git a/direct/src/extensions_native/NodePath_extensions.py b/direct/src/extensions_native/NodePath_extensions.py index 0ce11038a0..0c2c251941 100644 --- a/direct/src/extensions_native/NodePath_extensions.py +++ b/direct/src/extensions_native/NodePath_extensions.py @@ -435,7 +435,8 @@ Dtool_funcToMethod(iPosHprScale, NodePath) del iPosHprScale ##################################################################### def place(self): - base.startDirect(fWantTk = 1) + from direct.showbase import ShowBaseGlobal + ShowBaseGlobal.base.startDirect(fWantTk = 1) # Don't use a regular import, to prevent ModuleFinder from picking # it up as a dependency when building a .p3d package. import importlib @@ -446,7 +447,8 @@ Dtool_funcToMethod(place, NodePath) del place ##################################################################### def explore(self): - base.startDirect(fWantTk = 1) + from direct.showbase import ShowBaseGlobal + ShowBaseGlobal.base.startDirect(fWantTk = 1) # Don't use a regular import, to prevent ModuleFinder from picking # it up as a dependency when building a .p3d package. import importlib @@ -457,7 +459,8 @@ Dtool_funcToMethod(explore, NodePath) del explore ##################################################################### def rgbPanel(self, cb = None): - base.startTk() + from direct.showbase import ShowBaseGlobal + ShowBaseGlobal.base.startTk() # Don't use a regular import, to prevent ModuleFinder from picking # it up as a dependency when building a .p3d package. import importlib @@ -468,6 +471,8 @@ Dtool_funcToMethod(rgbPanel, NodePath) del rgbPanel ##################################################################### def select(self): + from direct.showbase import ShowBaseGlobal + base = ShowBaseGlobal.base base.startDirect(fWantTk = 0) base.direct.select(self) @@ -475,6 +480,8 @@ Dtool_funcToMethod(select, NodePath) del select ##################################################################### def deselect(self): + from direct.showbase import ShowBaseGlobal + base = ShowBaseGlobal.base base.startDirect(fWantTk = 0) base.direct.deselect(self) @@ -676,7 +683,8 @@ def flattenMultitex(self, stateFrom = None, target = None, mr.setAllowTexMat(allowTexMat) if win is None: - win = base.win + from direct.showbase import ShowBaseGlobal + win = ShowBaseGlobal.base.win if stateFrom is None: mr.scan(self) diff --git a/direct/src/showbase/ShowBase.py b/direct/src/showbase/ShowBase.py index 05d7612339..0a58cf74e9 100644 --- a/direct/src/showbase/ShowBase.py +++ b/direct/src/showbase/ShowBase.py @@ -431,6 +431,7 @@ class ShowBase(DirectObject.DirectObject): #: `.Loader.Loader` object. self.loader = Loader.Loader(self) self.graphicsEngine.setDefaultLoader(self.loader.loader) + ShowBaseGlobal.loader = self.loader #: The global event manager, as imported from `.EventManagerGlobal`. self.eventMgr = eventMgr diff --git a/direct/src/showbase/ShowBaseGlobal.py b/direct/src/showbase/ShowBaseGlobal.py index a9fd35c51a..3e656c045f 100644 --- a/direct/src/showbase/ShowBaseGlobal.py +++ b/direct/src/showbase/ShowBaseGlobal.py @@ -19,6 +19,7 @@ from panda3d.core import VirtualFileSystem, Notify, ClockObject, PandaSystem from panda3d.core import ConfigPageManager, ConfigVariableManager, ConfigVariableBool from panda3d.core import NodePath, PGTop from . import DConfig as config # pylint: disable=unused-import +from .Loader import Loader import warnings __dev__: bool = ConfigVariableBool('want-dev', __debug__).value @@ -61,6 +62,8 @@ aspect2d = render2d.attachNewNode(PGTop("aspect2d")) #: A dummy scene graph that is not being rendered by anything. hidden = NodePath("hidden") +loader: Loader + direct: "DirectSession" # Set direct notify categories now that we have config diff --git a/direct/src/showbase/TkGlobal.py b/direct/src/showbase/TkGlobal.py index 2ba0be0788..ef95fe5ef0 100644 --- a/direct/src/showbase/TkGlobal.py +++ b/direct/src/showbase/TkGlobal.py @@ -37,4 +37,5 @@ del bordercolors def spawnTkLoop(): """Alias for :meth:`base.spawnTkLoop() <.ShowBase.spawnTkLoop>`.""" - base.spawnTkLoop() + from direct.showbase import ShowBaseGlobal + ShowBaseGlobal.base.spawnTkLoop() diff --git a/direct/src/showbase/WxGlobal.py b/direct/src/showbase/WxGlobal.py index dd43c673b5..20c51236ad 100755 --- a/direct/src/showbase/WxGlobal.py +++ b/direct/src/showbase/WxGlobal.py @@ -3,4 +3,5 @@ def spawnWxLoop(): """Alias for :meth:`base.spawnWxLoop() <.ShowBase.spawnWxLoop>`.""" - base.spawnWxLoop() + from direct.showbase import ShowBaseGlobal + ShowBaseGlobal.base.spawnWxLoop() diff --git a/direct/src/task/MiniTask.py b/direct/src/task/MiniTask.py index bf13c57a5b..49a81a11fa 100755 --- a/direct/src/task/MiniTask.py +++ b/direct/src/task/MiniTask.py @@ -12,6 +12,8 @@ class MiniTask: done = 0 cont = 1 + name: str + def __init__(self, callback): self.__call__ = callback diff --git a/direct/src/wxwidgets/WxAppShell.py b/direct/src/wxwidgets/WxAppShell.py index 39da7eb224..23992b2690 100755 --- a/direct/src/wxwidgets/WxAppShell.py +++ b/direct/src/wxwidgets/WxAppShell.py @@ -79,13 +79,12 @@ class WxAppShell(wx.Frame): self.onDestroy(event) # to close Panda - try: - base - except NameError: + from direct.showbase import ShowBaseGlobal + if hasattr(ShowBaseGlobal, 'base'): + ShowBaseGlobal.base.userExit() + else: sys.exit() - base.userExit() - ### USER METHODS ### # To be overridden def appInit(self): From 79a60688cea128ba9cc4a35076f9f65ca57c0f92 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 11 Oct 2023 15:53:57 +0200 Subject: [PATCH 15/29] pnmimage: Support reading of .bmp files with RLE8 compression --- panda/src/pnmimagetypes/bmp.h | 14 +-- panda/src/pnmimagetypes/pnmFileTypeBMP.h | 1 + .../pnmimagetypes/pnmFileTypeBMPReader.cxx | 114 ++++++++++++------ 3 files changed, 82 insertions(+), 47 deletions(-) diff --git a/panda/src/pnmimagetypes/bmp.h b/panda/src/pnmimagetypes/bmp.h index d825bc71a1..2c5a08006f 100644 --- a/panda/src/pnmimagetypes/bmp.h +++ b/panda/src/pnmimagetypes/bmp.h @@ -92,19 +92,7 @@ BMPlenrgbtable(int classv, unsigned long bitcount) pm_error(er_internal, "BMPlenrgbtable"); return 0; } - switch (classv) - { - case C_WIN: - lenrgb = 4; - break; - case C_OS2: - lenrgb = 3; - break; - default: - pm_error(er_internal, "BMPlenrgbtable"); - return 0; - } - + lenrgb = (classv == C_OS2) ? 3 : 4; return (1 << bitcount) * lenrgb; } diff --git a/panda/src/pnmimagetypes/pnmFileTypeBMP.h b/panda/src/pnmimagetypes/pnmFileTypeBMP.h index 6cd3e8e32f..57fb6aacc4 100644 --- a/panda/src/pnmimagetypes/pnmFileTypeBMP.h +++ b/panda/src/pnmimagetypes/pnmFileTypeBMP.h @@ -55,6 +55,7 @@ public: unsigned long offBits; unsigned short cBitCount; + unsigned short cCompression; int indexed; int classv; diff --git a/panda/src/pnmimagetypes/pnmFileTypeBMPReader.cxx b/panda/src/pnmimagetypes/pnmFileTypeBMPReader.cxx index 7b2fc6e18c..638650bd2c 100644 --- a/panda/src/pnmimagetypes/pnmFileTypeBMPReader.cxx +++ b/panda/src/pnmimagetypes/pnmFileTypeBMPReader.cxx @@ -177,6 +177,7 @@ BMPreadinfoheader( unsigned long *pcx, unsigned long *pcy, unsigned short *pcBitCount, + unsigned short *pcCompression, int *pclassv) { unsigned long cbFix; @@ -185,6 +186,7 @@ BMPreadinfoheader( unsigned long cx = 0; unsigned long cy = 0; unsigned short cBitCount = 0; + unsigned long cCompression = 0; int classv = 0; cbFix = GetLong(fp); @@ -229,7 +231,9 @@ BMPreadinfoheader( * for the required total. */ if (classv != C_OS2) { - for (int i = 0; i < (int)cbFix - 16; i += 4) { + cCompression = GetLong(fp); + + for (int i = 0; i < (int)cbFix - 20; i += 4) { GetLong(fp); } } @@ -273,11 +277,13 @@ BMPreadinfoheader( pm_message("cy: %d", cy); pm_message("cPlanes: %d", cPlanes); pm_message("cBitCount: %d", cBitCount); + pm_message("cCompression: %d", cCompression); #endif *pcx = cx; *pcy = cy; *pcBitCount = cBitCount; + *pcCompression = cCompression; *pclassv = classv; *ppos += cbFix; @@ -401,45 +407,84 @@ BMPreadbits(xel *array, xelval *alpha_array, unsigned long cx, unsigned long cy, unsigned short cBitCount, - int /* classv */, + unsigned long cCompression, int indexed, pixval *R, pixval *G, pixval *B) { - long y; + long y; - readto(fp, ppos, offBits); + readto(fp, ppos, offBits); - if(cBitCount > 24 && cBitCount != 32) - { - pm_error("%s: cannot handle cBitCount: %d" - ,ifname - ,cBitCount); + if (cBitCount > 24 && cBitCount != 32) { + pm_error("%s: cannot handle cBitCount: %d", ifname, cBitCount); + } + + if (cCompression == 1) { + // RLE8 compression + xel *row = array + (cy - 1) * cx; + xel *p = row; + unsigned long nbyte = 0; + while (true) { + int first = GetByte(fp); + int second = GetByte(fp); + nbyte += 2; + + if (first != 0) { + // Repeated index. + for (int i = 0; i < first; ++i) { + PPM_ASSIGN(*p, R[second], G[second], B[second]); + ++p; } - - /* - * The picture is stored bottom line first, top line last - */ - - for (y = (long)cy - 1; y >= 0; y--) - { - int rc; - rc = BMPreadrow(fp, ppos, array + y*cx, alpha_array + y*cx, cx, cBitCount, indexed, R, G, B); - if(rc == -1) - { - pm_error("%s: couldn't read row %d" - ,ifname - ,y); - } - if(rc%4) - { - pm_error("%s: row had bad number of bytes: %d" - ,ifname - ,rc); - } + } + else if (second == 0) { + // End of line. + row -= cx; + p = row; + } + else if (second == 1) { + // End of image. + break; + } + else if (second == 2) { + // Delta. + int xoffset = GetByte(fp); + int yoffset = GetByte(fp); + nbyte += 2; + row -= cx * yoffset; + p += xoffset - cx * yoffset; + } + else { + // Absolute run. + for (int i = 0; i < second; ++i) { + int v = GetByte(fp); + ++nbyte; + PPM_ASSIGN(*p, R[v], G[v], B[v]); + ++p; } - + nbyte += second; + if (second % 2) { + // Pad to 16-bit boundary. + GetByte(fp); + ++nbyte; + } + } + } + *ppos += nbyte; + } + else { + // The picture is stored bottom line first, top line last + for (y = (long)cy - 1; y >= 0; y--) { + int rc = BMPreadrow(fp, ppos, array + y*cx, alpha_array + y*cx, cx, cBitCount, indexed, R, G, B); + if (rc == -1) { + pm_error("%s: couldn't read row %d", ifname, y); + } + if (rc % 4) { + pm_error("%s: row had bad number of bytes: %d", ifname, rc); + } + } + } } /** @@ -474,7 +519,7 @@ Reader(PNMFileType *type, istream *file, bool owns_file, string magic_number) : pos = 0; BMPreadfileheader(file, &pos, &offBits); - BMPreadinfoheader(file, &pos, &cx, &cy, &cBitCount, &classv); + BMPreadinfoheader(file, &pos, &cx, &cy, &cBitCount, &cCompression, &classv); if (offBits != BMPoffbits(classv, cBitCount)) { pnmimage_bmp_cat.warning() @@ -523,9 +568,10 @@ Reader(PNMFileType *type, istream *file, bool owns_file, string magic_number) : int PNMFileTypeBMP::Reader:: read_data(xel *array, xelval *alpha_array) { BMPreadbits(array, alpha_array, _file, &pos, offBits, _x_size, _y_size, - cBitCount, classv, indexed, R, G, B); + cBitCount, cCompression, indexed, R, G, B); - if (pos != BMPlenfile(classv, cBitCount, _x_size, _y_size)) { + if (cCompression != 1 && + pos != BMPlenfile(classv, cBitCount, _x_size, _y_size)) { pnmimage_bmp_cat.warning() << "Read " << pos << " bytes, expected to read " << BMPlenfile(classv, cBitCount, _x_size, _y_size) << " bytes\n"; From 5685949588d8dec4817b3f33c99743e43a298369 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 11 Oct 2023 16:07:20 +0200 Subject: [PATCH 16/29] shader: Fix regression fetching material shader inputs --- panda/src/gobj/shader.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panda/src/gobj/shader.cxx b/panda/src/gobj/shader.cxx index 55fec70437..067ab9527c 100644 --- a/panda/src/gobj/shader.cxx +++ b/panda/src/gobj/shader.cxx @@ -652,7 +652,6 @@ cp_add_mat_spec(ShaderMatSpec &spec) { case SMO_pixel_size: case SMO_texpad_x: case SMO_texpix_x: - case SMO_attr_material: case SMO_attr_color: case SMO_attr_colorscale: case SMO_satten_x: @@ -684,6 +683,7 @@ cp_add_mat_spec(ShaderMatSpec &spec) { break; case SMO_identity: + case SMO_attr_material: case SMO_alight_x: case SMO_dlight_x: case SMO_plight_x: From ad8882123b67ea0a729ab956000603963a1d83ae Mon Sep 17 00:00:00 2001 From: David Crompton Date: Sun, 15 Jan 2023 13:35:19 +0000 Subject: [PATCH 17/29] Panda3DToolsGUI: Update setup.py to use setuptools and change Print statements to Python3 syntax --- contrib/src/panda3dtoolsgui/Panda3DToolsGUI.py | 6 +++--- contrib/src/panda3dtoolsgui/setup.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contrib/src/panda3dtoolsgui/Panda3DToolsGUI.py b/contrib/src/panda3dtoolsgui/Panda3DToolsGUI.py index 331081016b..abd609a123 100644 --- a/contrib/src/panda3dtoolsgui/Panda3DToolsGUI.py +++ b/contrib/src/panda3dtoolsgui/Panda3DToolsGUI.py @@ -2650,11 +2650,11 @@ class main(wx.Frame): for inputFile in inputs: if (inputFile != ''): inputFilename = inputFile.split('\\')[-1] - print "Compare: ", inFile, filename, inputFile, inputFilename + print("Compare: ", inFile, filename, inputFile, inputFilename) if inputFilename == filename: inputTime = os.path.getmtime(inputFile) outputTime = os.path.getmtime(inFile) - print "Matched: ", (inputTime > outputTime) + print("Matched: ", (inputTime > outputTime)) inputChanged = (inputTime > outputTime) break ''' @@ -2848,7 +2848,7 @@ class main(wx.Frame): except ValueError: return - #print self.batchList + #print(self.batchList) def OnBatchItemEdit(self, event): selectedItemId = self.batchTree.GetSelections() diff --git a/contrib/src/panda3dtoolsgui/setup.py b/contrib/src/panda3dtoolsgui/setup.py index 8691c87923..d79dbc244e 100644 --- a/contrib/src/panda3dtoolsgui/setup.py +++ b/contrib/src/panda3dtoolsgui/setup.py @@ -1,4 +1,4 @@ -from distutils.core import setup +from setuptools import setup import py2exe setup(console=['Panda3DToolsGUI.py']) From 521cad206d980026509c671bc6ad42058b92c06b Mon Sep 17 00:00:00 2001 From: Mitchell Stokes Date: Wed, 11 Oct 2023 19:39:01 -0700 Subject: [PATCH 18/29] makepanda: Stop using deprecated distutils (#1549) Just duplicating locations.py from direct. It's a bit ugly, but makepanda is getting phased out anyways. Co-authored-by: rdb --- makepanda/installpanda.py | 2 +- makepanda/locations.py | 32 ++++++++++++++++++++++++++++++++ makepanda/makepackage.py | 2 +- makepanda/makepanda.py | 3 ++- makepanda/makepandacore.py | 27 +++++++++++++-------------- makepanda/makewheel.py | 4 ++-- 6 files changed, 51 insertions(+), 19 deletions(-) create mode 100644 makepanda/locations.py diff --git a/makepanda/installpanda.py b/makepanda/installpanda.py index d0111e0049..89e32c765e 100644 --- a/makepanda/installpanda.py +++ b/makepanda/installpanda.py @@ -9,9 +9,9 @@ ######################################################################## import os, sys, platform -from distutils.sysconfig import get_python_lib from optparse import OptionParser from makepandacore import * +from locations import get_python_lib MIME_INFO = ( diff --git a/makepanda/locations.py b/makepanda/locations.py new file mode 100644 index 0000000000..d410b77aff --- /dev/null +++ b/makepanda/locations.py @@ -0,0 +1,32 @@ +__all__ = [ + 'get_python_inc', + 'get_config_var', + 'get_python_version', + 'PREFIX', + 'get_python_lib', + 'get_config_vars', +] + +import sys + +if sys.version_info < (3, 12): + from distutils.sysconfig import * +else: + from sysconfig import * + + PREFIX = get_config_var('prefix') + + def get_python_inc(plat_specific=False): + path_name = 'platinclude' if plat_specific else 'include' + return get_path(path_name) + + def get_python_lib(plat_specific=False, standard_lib=False): + if standard_lib: + path_name = 'stdlib' + if plat_specific: + path_name = 'plat' + path_name + elif plat_specific: + path_name = 'platlib' + else: + path_name = 'purelib' + return get_path(path_name) diff --git a/makepanda/makepackage.py b/makepanda/makepackage.py index f57fc873e3..68c74523dc 100755 --- a/makepanda/makepackage.py +++ b/makepanda/makepackage.py @@ -1032,7 +1032,7 @@ def MakeInstallerAndroid(version, **kwargs): shutil.copy(os.path.join(source_dir, base), target) # Copy the Python standard library to the .apk as well. - from distutils.sysconfig import get_python_lib + from locations import get_python_lib stdlib_source = get_python_lib(False, True) stdlib_target = os.path.join("apkroot", "lib", "python{0}.{1}".format(*sys.version_info)) copy_python_tree(stdlib_source, stdlib_target) diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index 407c95bb33..d88a1cb153 100755 --- a/makepanda/makepanda.py +++ b/makepanda/makepanda.py @@ -23,11 +23,12 @@ except: exit(1) from makepandacore import * -from distutils.util import get_platform import time import os import sys +from sysconfig import get_platform + ######################################################################## ## ## PARSING THE COMMAND LINE OPTIONS diff --git a/makepanda/makepandacore.py b/makepanda/makepandacore.py index ad75ae6f1d..111a3a6717 100644 --- a/makepanda/makepandacore.py +++ b/makepanda/makepandacore.py @@ -7,7 +7,7 @@ import sys,os,time,stat,string,re,getopt,fnmatch,threading,signal,shutil,platform,glob,getpass,signal import subprocess -from distutils import sysconfig +import locations if sys.version_info >= (3, 0): import pickle @@ -2250,7 +2250,7 @@ def SdkLocatePython(prefer_thirdparty_python=False): # On macOS, search for the Python framework directory matching the # version number of our current Python version. sysroot = SDK.get("MACOSX", "") - version = sysconfig.get_python_version() + version = locations.get_python_version() py_fwx = "{0}/System/Library/Frameworks/Python.framework/Versions/{1}".format(sysroot, version) @@ -2275,19 +2275,19 @@ def SdkLocatePython(prefer_thirdparty_python=False): LibDirectory("PYTHON", py_fwx + "/lib") #elif GetTarget() == 'windows': - # SDK["PYTHON"] = os.path.dirname(sysconfig.get_python_inc()) - # SDK["PYTHONVERSION"] = "python" + sysconfig.get_python_version() + # SDK["PYTHON"] = os.path.dirname(locations.get_python_inc()) + # SDK["PYTHONVERSION"] = "python" + locations.get_python_version() # SDK["PYTHONEXEC"] = sys.executable else: - SDK["PYTHON"] = sysconfig.get_python_inc() - SDK["PYTHONVERSION"] = "python" + sysconfig.get_python_version() + abiflags + SDK["PYTHON"] = locations.get_python_inc() + SDK["PYTHONVERSION"] = "python" + locations.get_python_version() + abiflags SDK["PYTHONEXEC"] = os.path.realpath(sys.executable) if CrossCompiling(): # We need a version of Python we can run. SDK["PYTHONEXEC"] = sys.executable - host_version = "python" + sysconfig.get_python_version() + abiflags + host_version = "python" + locations.get_python_version() + abiflags if SDK["PYTHONVERSION"] != host_version: exit("Host Python version (%s) must be the same as target Python version (%s)!" % (host_version, SDK["PYTHONVERSION"])) @@ -3514,7 +3514,7 @@ def GetExtensionSuffix(): return '.so' def GetPythonABI(): - soabi = sysconfig.get_config_var('SOABI') + soabi = locations.get_config_var('SOABI') if soabi: return soabi @@ -3523,16 +3523,16 @@ def GetPythonABI(): if sys.version_info >= (3, 8): return soabi - debug_flag = sysconfig.get_config_var('Py_DEBUG') + debug_flag = locations.get_config_var('Py_DEBUG') if (debug_flag is None and hasattr(sys, 'gettotalrefcount')) or debug_flag: soabi += 'd' - malloc_flag = sysconfig.get_config_var('WITH_PYMALLOC') + malloc_flag = locations.get_config_var('WITH_PYMALLOC') if malloc_flag is None or malloc_flag: soabi += 'm' if sys.version_info < (3, 3): - usize = sysconfig.get_config_var('Py_UNICODE_SIZE') + usize = locations.get_config_var('Py_UNICODE_SIZE') if (usize is None and sys.maxunicode == 0x10ffff) or usize == 4: soabi += 'u' @@ -3648,14 +3648,13 @@ def GetCurrentPythonVersionInfo(): if PkgSkip("PYTHON"): return - from distutils.sysconfig import get_python_lib return { "version": SDK["PYTHONVERSION"][6:].rstrip('dmu'), "soabi": GetPythonABI(), "ext_suffix": GetExtensionSuffix(), "executable": sys.executable, - "purelib": get_python_lib(False), - "platlib": get_python_lib(True), + "purelib": locations.get_python_lib(False), + "platlib": locations.get_python_lib(True), } diff --git a/makepanda/makewheel.py b/makepanda/makewheel.py index 2f18789e78..55a48f62cf 100644 --- a/makepanda/makewheel.py +++ b/makepanda/makewheel.py @@ -2,7 +2,6 @@ Generates a wheel (.whl) file from the output of makepanda. """ from __future__ import print_function, unicode_literals -from distutils.util import get_platform import json import sys @@ -13,10 +12,11 @@ import zipfile import hashlib import tempfile import subprocess -from distutils.sysconfig import get_config_var from optparse import OptionParser from makepandacore import ColorText, LocateBinary, GetExtensionSuffix, SetVerbose, GetVerbose, GetMetadataValue from base64 import urlsafe_b64encode +from locations import get_config_var +from sysconfig import get_platform def get_abi_tag(): From 893f5ce4921d377e3ff9a7bef48846cf24a1c4d5 Mon Sep 17 00:00:00 2001 From: Mitchell Stokes Date: Thu, 12 Oct 2023 16:42:17 -0700 Subject: [PATCH 19/29] Fix assert on Py_SIZE(long) when using Python 3.12 Starting with Python 3.12, passing a PyLong into Py_SIZE() triggers an assertion. PyLong (and the whole C API) is transitioning to be more opaque and expose fewer implementation details. --- dtool/src/interrogatedb/py_compat.h | 12 ++++++++++++ panda/src/putil/bitArray_ext.cxx | 4 ++-- panda/src/putil/doubleBitMask_ext.I | 3 +-- tests/putil/test_bitarray.py | 3 +++ 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/dtool/src/interrogatedb/py_compat.h b/dtool/src/interrogatedb/py_compat.h index c5474b21ee..20946e7f48 100644 --- a/dtool/src/interrogatedb/py_compat.h +++ b/dtool/src/interrogatedb/py_compat.h @@ -243,6 +243,18 @@ INLINE PyObject *PyObject_CallMethodOneArg(PyObject *obj, PyObject *name, PyObje } #endif +/* Python 3.12 */ + +#if PY_VERSION_HEX < 0x030C0000 +# define PyLong_IsNonNegative(value) (Py_SIZE((value)) >= 0) +#else +INLINE bool PyLong_IsNonNegative(PyObject *value) { + int overflow = 0; + long longval = PyLong_AsLongAndOverflow(value, &overflow); + return overflow == 1 || longval >= 0; +} +#endif + /* Other Python implementations */ // _PyErr_OCCURRED is an undocumented macro version of PyErr_Occurred. diff --git a/panda/src/putil/bitArray_ext.cxx b/panda/src/putil/bitArray_ext.cxx index 215d920cb4..6e0d860238 100644 --- a/panda/src/putil/bitArray_ext.cxx +++ b/panda/src/putil/bitArray_ext.cxx @@ -32,7 +32,7 @@ __init__(PyObject *init_value) { } #endif - if (!PyLong_Check(init_value) || Py_SIZE(init_value) < 0) { + if (!PyLong_Check(init_value) || !PyLong_IsNonNegative(init_value)) { PyErr_SetString(PyExc_ValueError, "BitArray constructor requires a positive integer"); return; } @@ -88,7 +88,7 @@ __getstate__() const { */ void Extension:: __setstate__(PyObject *state) { - if (Py_SIZE(state) >= 0) { + if (PyLong_IsNonNegative(state)) { __init__(state); } else { PyObject *inverted = PyNumber_Invert(state); diff --git a/panda/src/putil/doubleBitMask_ext.I b/panda/src/putil/doubleBitMask_ext.I index b774f22096..748def0371 100644 --- a/panda/src/putil/doubleBitMask_ext.I +++ b/panda/src/putil/doubleBitMask_ext.I @@ -28,8 +28,7 @@ __init__(PyObject *init_value) { return; } #endif - - if (!PyLong_Check(init_value) || Py_SIZE(init_value) < 0) { + if (!PyLong_Check(init_value) || !PyLong_IsNonNegative(init_value)) { PyErr_SetString(PyExc_ValueError, "DoubleBitMask constructor requires a positive integer"); return; } diff --git a/tests/putil/test_bitarray.py b/tests/putil/test_bitarray.py index 5da2fdc725..293be8df74 100644 --- a/tests/putil/test_bitarray.py +++ b/tests/putil/test_bitarray.py @@ -32,6 +32,9 @@ def test_bitarray_pickle(): ba = BitArray(123) assert ba == pickle.loads(pickle.dumps(ba, -1)) + ba = BitArray(1 << 128) + assert ba == pickle.loads(pickle.dumps(ba, -1)) + def test_bitarray_has_any_of(): ba = BitArray() From 225b577ccd9c0725158824c3ff697a5b3d798f40 Mon Sep 17 00:00:00 2001 From: rdb Date: Fri, 13 Oct 2023 10:55:17 +0200 Subject: [PATCH 20/29] tests: Skip Cg tests on arm64 machines --- tests/display/test_cg_shader.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/display/test_cg_shader.py b/tests/display/test_cg_shader.py index 8a6c4d7a74..2597b39971 100644 --- a/tests/display/test_cg_shader.py +++ b/tests/display/test_cg_shader.py @@ -1,4 +1,6 @@ import os +import platform +import pytest from panda3d import core @@ -16,12 +18,14 @@ def run_cg_compile_check(gsg, shader_path, expect_fail=False): assert shader is not None +@pytest.mark.skipif(platform.machine().lower() == 'arm64', reason="Cg not supported on arm64") def test_cg_compile_error(gsg): """Test getting compile errors from bad Cg shaders""" shader_path = core.Filename(SHADERS_DIR, 'cg_bad.sha') run_cg_compile_check(gsg, shader_path, expect_fail=True) +@pytest.mark.skipif(platform.machine().lower() == 'arm64', reason="Cg not supported on arm64") def test_cg_from_file(gsg): """Test compiling Cg shaders from files""" shader_path = core.Filename(SHADERS_DIR, 'cg_simple.sha') From 7f0eafcc27940e753c1ff8dd5facf2322fba5f72 Mon Sep 17 00:00:00 2001 From: rdb Date: Fri, 13 Oct 2023 12:10:08 +0200 Subject: [PATCH 21/29] workflow: Disable Python 3.7 CI, enable Python 3.12 CI --- .github/workflows/ci.yml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72795cdd0a..3c14a39546 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,20 @@ jobs: rmdir panda3d-1.10.13 (cd thirdparty/darwin-libs-a && rm -rf rocket) + - name: Set up Python 3.12 + uses: actions/setup-python@v4 + with: + python-version: '3.12' + - name: Build Python 3.12 + shell: bash + run: | + python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 + - name: Test Python 3.12 + shell: bash + run: | + python -m pip install pytest + PYTHONPATH=built LD_LIBRARY_PATH=built/lib DYLD_LIBRARY_PATH=built/lib python -m pytest + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: @@ -88,20 +102,6 @@ jobs: python -m pip install pytest PYTHONPATH=built LD_LIBRARY_PATH=built/lib DYLD_LIBRARY_PATH=built/lib python -m pytest - - name: Set up Python 3.7 - uses: actions/setup-python@v4 - with: - python-version: '3.7' - - name: Build Python 3.7 - shell: bash - run: | - python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 - - name: Test Python 3.7 - shell: bash - run: | - python -m pip install pytest - PYTHONPATH=built LD_LIBRARY_PATH=built/lib DYLD_LIBRARY_PATH=built/lib python -m pytest - - name: Make installer run: | python makepanda/makepackage.py --verbose --lzma From 2a5228b05f6692668bc819d3101c1ac6c4d8441a Mon Sep 17 00:00:00 2001 From: rdb Date: Fri, 13 Oct 2023 13:20:26 +0200 Subject: [PATCH 22/29] Fix compatibility with Python 3.12 by removing use of imp module Some modules (such as VFSImporter, and various modules in direct.p3d that depend on it) are still unavailable. --- direct/src/dist/FreezeTool.py | 197 +++++++++++++++++++------ direct/src/dist/commands.py | 5 +- direct/src/dist/icon.py | 265 ++++++++++++++++++++++++++++++++++ makepanda/test_imports.py | 36 +++-- tests/dist/test_FreezeTool.py | 64 ++++++++ 5 files changed, 503 insertions(+), 64 deletions(-) create mode 100644 direct/src/dist/icon.py create mode 100644 tests/dist/test_FreezeTool.py diff --git a/direct/src/dist/FreezeTool.py b/direct/src/dist/FreezeTool.py index 3cfdafe1e9..1ca9cf1c36 100644 --- a/direct/src/dist/FreezeTool.py +++ b/direct/src/dist/FreezeTool.py @@ -5,7 +5,6 @@ import modulefinder import sys import os import marshal -import imp import platform import struct import io @@ -18,6 +17,12 @@ import importlib from . import pefile +if sys.version_info >= (3, 4): + import _imp + from importlib import machinery +else: + import imp + # Temporary (?) try..except to protect against unbuilt p3extend_frozen. try: import p3extend_frozen @@ -26,6 +31,16 @@ except ImportError: from panda3d.core import * +# Old imp constants. +_PY_SOURCE = 1 +_PY_COMPILED = 2 +_C_EXTENSION = 3 +_PKG_DIRECTORY = 5 +_C_BUILTIN = 6 +_PY_FROZEN = 7 + +_PKG_NAMESPACE_DIRECTORY = object() + # Check to see if we are running python_d, which implies we have a # debug build, and we have to build the module with debug options. # This is only relevant on Windows. @@ -39,8 +54,11 @@ isDebugBuild = (python.lower().endswith('_d')) # NB. if encodings are removed, be sure to remove them from the shortcut in # deploy-stub.c. startupModules = [ - 'imp', 'encodings', 'encodings.*', + 'encodings', 'encodings.*', ] +if sys.version_info < (3, 12): + startupModules.insert(0, 'imp') + if sys.version_info >= (3, 0): # Modules specific to Python 3 startupModules += ['io', 'marshal', 'importlib.machinery', 'importlib.util'] @@ -885,12 +903,19 @@ class Freezer: # Suffix/extension for Python C extension modules if self.platform == PandaSystem.getPlatform(): - self.moduleSuffixes = imp.get_suffixes() + if sys.version_info >= (3, 4): + self.moduleSuffixes = ( + [(s, 'rb', _C_EXTENSION) for s in machinery.EXTENSION_SUFFIXES] + + [(s, 'rb', _PY_SOURCE) for s in machinery.SOURCE_SUFFIXES] + + [(s, 'rb', _PY_COMPILED) for s in machinery.BYTECODE_SUFFIXES] + ) + else: + self.moduleSuffixes = imp.get_suffixes() - # Set extension for Python files to binary mode - for i, suffix in enumerate(self.moduleSuffixes): - if suffix[2] == imp.PY_SOURCE: - self.moduleSuffixes[i] = (suffix[0], 'rb', imp.PY_SOURCE) + # Set extension for Python files to binary mode + for i, suffix in enumerate(self.moduleSuffixes): + if suffix[2] == _PY_SOURCE: + self.moduleSuffixes[i] = (suffix[0], 'rb', _PY_SOURCE) else: self.moduleSuffixes = [('.py', 'rb', 1), ('.pyc', 'rb', 2)] @@ -990,21 +1015,45 @@ class Freezer: # whatever--then just look for file on disk. That's usually # good enough. path = None - baseName = moduleName - if '.' in baseName: - parentName, baseName = moduleName.rsplit('.', 1) + name = moduleName + if '.' in name: + parentName, name = moduleName.rsplit('.', 1) path = self.getModulePath(parentName) if path is None: return None - try: - file, pathname, description = imp.find_module(baseName, path) - except ImportError: - return None + if sys.version_info < (3, 4): + try: + file, pathname, description = imp.find_module(name, path) + except ImportError: + return None - if not os.path.isdir(pathname): - return None - return [pathname] + if not os.path.isdir(pathname): + return None + return [pathname] + + if path is None: + if _imp.is_builtin(name) or _imp.is_frozen(name): + return None + + path = sys.path + + for entry in path: + package_directory = os.path.join(entry, name) + for suffix in ('.py', machinery.BYTECODE_SUFFIXES[0]): + package_file_name = '__init__' + suffix + file_path = os.path.join(package_directory, package_file_name) + if os.path.isfile(file_path): + return [package_directory] + + for suffix in machinery.EXTENSION_SUFFIXES + machinery.SOURCE_SUFFIXES + machinery.BYTECODE_SUFFIXES: + file_name = name + suffix + file_path = os.path.join(entry, file_name) + if os.path.isfile(file_path): + # Not a package. + return None + + return None def getModuleStar(self, moduleName): """ Looks for the indicated directory module and returns the @@ -1027,20 +1076,49 @@ class Freezer: # If it didn't work, just open the directory and scan for *.py # files. path = None - baseName = moduleName - if '.' in baseName: - parentName, baseName = moduleName.rsplit('.', 1) + name = moduleName + if '.' in name: + parentName, name = moduleName.rsplit('.', 1) path = self.getModulePath(parentName) if path is None: return None - try: - file, pathname, description = imp.find_module(baseName, path) - except ImportError: - return None + if sys.version_info < (3, 4): + try: + file, pathname, description = imp.find_module(name, path) + except ImportError: + return None - if not os.path.isdir(pathname): - return None + if not os.path.isdir(pathname): + return None + else: + if path is None: + if _imp.is_builtin(name) or _imp.is_frozen(name): + return None + + path = sys.path + + for entry in path: + package_directory = os.path.join(entry, name) + for suffix in ('.py', machinery.BYTECODE_SUFFIXES[0]): + package_file_name = '__init__' + suffix + file_path = os.path.join(package_directory, package_file_name) + if os.path.isfile(file_path): + pathname = package_directory + break + else: + for suffix in machinery.EXTENSION_SUFFIXES + machinery.SOURCE_SUFFIXES + machinery.BYTECODE_SUFFIXES: + file_name = name + suffix + file_path = os.path.join(entry, file_name) + if os.path.isfile(file_path): + # Not a package. + return None + else: + continue + + break # Break out of outer loop when breaking out of inner loop. + else: + return None # Scan the directory, looking for .py files. modules = [] @@ -1335,10 +1413,10 @@ class Freezer: ext = mdef.filename.getExtension() if ext == 'pyc' or ext == 'pyo': fp = open(pathname, 'rb') - stuff = ("", "rb", imp.PY_COMPILED) + stuff = ("", "rb", _PY_COMPILED) self.mf.load_module(mdef.moduleName, fp, pathname, stuff) else: - stuff = ("", "rb", imp.PY_SOURCE) + stuff = ("", "rb", _PY_SOURCE) if mdef.text: fp = io.StringIO(mdef.text) else: @@ -1434,10 +1512,10 @@ class Freezer: def __addPyc(self, multifile, filename, code, compressionLevel): if code: - data = imp.get_magic() + b'\0\0\0\0' - - if sys.version_info >= (3, 0): - data += b'\0\0\0\0' + if sys.version_info >= (3, 4): + data = importlib.util.MAGIC_NUMBER + b'\0\0\0\0\0\0\0\0' + else: + data = imp.get_magic() + b'\0\0\0\0' data += marshal.dumps(code) @@ -1634,7 +1712,11 @@ class Freezer: # trouble importing it as a builtin module. Synthesize a frozen # module that loads it as builtin. if '.' in moduleName and self.linkExtensionModules: - if sys.version_info >= (3, 2): + if sys.version_info >= (3, 5): + code = compile('import sys;del sys.modules["%s"];from importlib._bootstrap import _builtin_from_name;_builtin_from_name("%s")' % (moduleName, moduleName), moduleName, 'exec', optimize=self.optimize) + elif sys.version_info >= (3, 4): + code = compile('import sys;del sys.modules["%s"];import _imp;_imp.init_builtin("%s")' % (moduleName, moduleName), moduleName, 'exec', optimize=self.optimize) + elif sys.version_info >= (3, 2): code = compile('import sys;del sys.modules["%s"];import imp;imp.init_builtin("%s")' % (moduleName, moduleName), moduleName, 'exec', optimize=self.optimize) else: code = compile('import sys;del sys.modules["%s"];import imp;imp.init_builtin("%s")' % (moduleName, moduleName), moduleName, 'exec') @@ -1910,9 +1992,25 @@ class Freezer: if '.' in moduleName: if self.platform.startswith("macosx") and not use_console: # We write the Frameworks directory to sys.path[0]. - code = 'import sys;del sys.modules["%s"];import sys,os,imp;imp.load_dynamic("%s",os.path.join(sys.path[0], "%s%s"))' % (moduleName, moduleName, moduleName, modext) + direxpr = 'sys.path[0]' else: - code = 'import sys;del sys.modules["%s"];import sys,os,imp;imp.load_dynamic("%s",os.path.join(os.path.dirname(sys.executable), "%s%s"))' % (moduleName, moduleName, moduleName, modext) + direxpr = 'os.path.dirname(sys.executable)' + + if sys.version_info >= (3, 5): + code = \ + 'import sys;' \ + 'del sys.modules["{name}"];' \ + 'import sys,os;' \ + 'from importlib.machinery import ExtensionFileLoader,ModuleSpec;' \ + 'from importlib._bootstrap import _load;' \ + 'path=os.path.join({direxpr}, "{name}{ext}");' \ + '_load(ModuleSpec(name="{name}", loader=ExtensionFileLoader("{name}", path), origin=path))' \ + ''.format(name=moduleName, ext=modext, direxpr=direxpr) + elif sys.version_info >= (3, 4): + code = 'import sys;del sys.modules["%s"];import sys,os,_imp;_imp.load_dynamic("%s",os.path.join(%s, "%s%s"))' % (moduleName, moduleName, direxpr, moduleName, modext) + else: + code = 'import sys;del sys.modules["%s"];import sys,os,imp;imp.load_dynamic("%s",os.path.join(%s, "%s%s"))' % (moduleName, moduleName, direxpr, moduleName, modext) + if sys.version_info >= (3, 2): code = compile(code, moduleName, 'exec', optimize=self.optimize) else: @@ -2362,9 +2460,6 @@ class Freezer: return True -_PKG_NAMESPACE_DIRECTORY = object() - - class PandaModuleFinder(modulefinder.ModuleFinder): def __init__(self, *args, **kw): @@ -2375,7 +2470,17 @@ class PandaModuleFinder(modulefinder.ModuleFinder): :param debug: an integer indicating the level of verbosity """ - self.suffixes = kw.pop('suffixes', imp.get_suffixes()) + if 'suffixes' in kw: + self.suffixes = kw.pop('suffixes') + elif sys.version_info >= (3, 4): + self.suffixes = ( + [(s, 'rb', _C_EXTENSION) for s in machinery.EXTENSION_SUFFIXES] + + [(s, 'r', _PY_SOURCE) for s in machinery.SOURCE_SUFFIXES] + + [(s, 'rb', _PY_COMPILED) for s in machinery.BYTECODE_SUFFIXES] + ) + else: + self.suffixes = imp.get_suffixes() + self.optimize = kw.pop('optimize', -1) modulefinder.ModuleFinder.__init__(self, *args, **kw) @@ -2475,7 +2580,7 @@ class PandaModuleFinder(modulefinder.ModuleFinder): suffix, mode, type = file_info self.msgin(2, "load_module", fqname, fp and "fp", pathname) - if type == imp.PKG_DIRECTORY: + if type == _PKG_DIRECTORY: m = self.load_package(fqname, pathname) self.msgout(2, "load_module ->", m) return m @@ -2489,7 +2594,7 @@ class PandaModuleFinder(modulefinder.ModuleFinder): m.__path__ = pathname return m - if type == imp.PY_SOURCE: + if type == _PY_SOURCE: if fqname in overrideModules: # This module has a custom override. code = overrideModules[fqname] @@ -2516,7 +2621,7 @@ class PandaModuleFinder(modulefinder.ModuleFinder): co = compile(code, pathname, 'exec', optimize=self.optimize) else: co = compile(code, pathname, 'exec') - elif type == imp.PY_COMPILED: + elif type == _PY_COMPILED: if sys.version_info >= (3, 7): try: data = fp.read() @@ -2681,12 +2786,12 @@ class PandaModuleFinder(modulefinder.ModuleFinder): # If we have a custom override for this module, we know we have it. if fullname in overrideModules: - return (None, '', ('.py', 'r', imp.PY_SOURCE)) + return (None, '', ('.py', 'r', _PY_SOURCE)) # If no search path is given, look for a built-in module. if path is None: if name in sys.builtin_module_names: - return (None, None, ('', '', imp.C_BUILTIN)) + return (None, None, ('', '', _C_BUILTIN)) path = self.path @@ -2718,7 +2823,7 @@ class PandaModuleFinder(modulefinder.ModuleFinder): for suffix, mode, _ in self.suffixes: init = os.path.join(basename, '__init__' + suffix) if self._open_file(init, mode): - return (None, basename, ('', '', imp.PKG_DIRECTORY)) + return (None, basename, ('', '', _PKG_DIRECTORY)) # This may be a namespace package. if self._dir_exists(basename): @@ -2730,7 +2835,7 @@ class PandaModuleFinder(modulefinder.ModuleFinder): # Only if we're not looking on a particular path, though. if p3extend_frozen and p3extend_frozen.is_frozen_module(name): # It's a frozen module. - return (None, name, ('', '', imp.PY_FROZEN)) + return (None, name, ('', '', _PY_FROZEN)) # If we found folders on the path with this module name without an # __init__.py file, we should consider this a namespace package. diff --git a/direct/src/dist/commands.py b/direct/src/dist/commands.py index a37f043849..e402b3c016 100644 --- a/direct/src/dist/commands.py +++ b/direct/src/dist/commands.py @@ -15,7 +15,6 @@ import re import shutil import stat import struct -import imp import string import time import tempfile @@ -25,7 +24,7 @@ import distutils.log from . import FreezeTool from . import pefile -from direct.p3d.DeploymentTools import Icon +from .icon import Icon import panda3d.core as p3d @@ -912,7 +911,7 @@ class build_apps(setuptools.Command): for mod in freezer.getModuleDefs() if mod[1].filename }) for suffix in freezer.moduleSuffixes: - if suffix[2] == imp.C_EXTENSION: + if suffix[2] == 3: # imp.C_EXTENSION: ext_suffixes.add(suffix[0]) for appname, scriptname in self.gui_apps.items(): diff --git a/direct/src/dist/icon.py b/direct/src/dist/icon.py new file mode 100644 index 0000000000..4a34cb95af --- /dev/null +++ b/direct/src/dist/icon.py @@ -0,0 +1,265 @@ +from direct.directnotify.DirectNotifyGlobal import directNotify +from panda3d.core import PNMImage, Filename, PNMFileTypeRegistry, StringStream +import struct + + +class Icon: + """ This class is used to create an icon for various platforms. """ + notify = directNotify.newCategory("Icon") + + def __init__(self): + self.images = {} + + def addImage(self, image): + """ Adds an image to the icon. Returns False on failure, True on success. + Only one image per size can be loaded, and the image size must be square. """ + + if not isinstance(image, PNMImage): + fn = image + if not isinstance(fn, Filename): + fn = Filename.fromOsSpecific(fn) + + image = PNMImage() + if not image.read(fn): + Icon.notify.warning("Image '%s' could not be read" % fn.getBasename()) + return False + + if image.getXSize() != image.getYSize(): + Icon.notify.warning("Ignoring image without square size") + return False + + self.images[image.getXSize()] = image + + 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: + from_size = 0 + 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) + image.setColorType(self.images[from_size].getColorType()) + 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('> 3) & 3) + for y in range(size): + mask = 0 + num_bits = 7 + for x in range(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('> 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. """ + + if not isinstance(fn, Filename): + fn = Filename.fromOsSpecific(fn) + fn.setBinary() + + # ICO files only support resolutions up to 256x256. + count = 0 + for size in self.images: + if size < 256: + count += 1 + if size <= 256: + count += 1 + dataoffs = 6 + count * 16 + + ico = open(fn, 'wb') + ico.write(struct.pack('= 256: + continue + ico.write(struct.pack('> 3 + if andsize % 4 != 0: + andsize += 4 - (andsize % 4) + datasize = 40 + 256 * 4 + (xorsize + andsize) * size + + ico.write(struct.pack(' 256: + continue + elif size == 256: + ico.write(b'\0\0') + else: + ico.write(struct.pack('> 3 + if andsize % 4 != 0: + andsize += 4 - (andsize % 4) + datasize = 40 + (xorsize + andsize) * size + + ico.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 range(size): + for x in range(size): + icns.write(struct.pack('I', size * size * 4 + 8)) + + for y in range(size): + for x in range(size): + r, g, b = image.getXel(x, y) + icns.write(struct.pack('>BBBB', 0, int(r * 255), int(g * 255), int(b * 255))) + + length = icns.tell() + icns.seek(4) + icns.write(struct.pack('>I', length)) + icns.close() + + return True diff --git a/makepanda/test_imports.py b/makepanda/test_imports.py index a9b22943c8..1ba808eca4 100644 --- a/makepanda/test_imports.py +++ b/makepanda/test_imports.py @@ -6,15 +6,19 @@ import os, importlib # This will print out imports on the command line. import direct.showbase.VerboseImport +import sys +if sys.version_info >= (3, 4): + from importlib import machinery + extensions = machinery.EXTENSION_SUFFIXES + machinery.SOURCE_SUFFIXES + machinery.BYTECODE_SUFFIXES +else: + import imp + extensions = set() + for suffix in imp.get_suffixes(): + extensions.add(suffix[0]) -import imp import panda3d dir = os.path.dirname(panda3d.__file__) -extensions = set() -for suffix in imp.get_suffixes(): - extensions.add(suffix[0]) - for basename in os.listdir(dir): module = basename.split('.', 1)[0] ext = basename[len(module):] @@ -159,18 +163,19 @@ import direct.interval.ProjectileIntervalTest import direct.interval.SoundInterval import direct.interval.TestInterval import direct.motiontrail.MotionTrail -import direct.p3d.AppRunner -import direct.p3d.DWBPackageInstaller -import direct.p3d.DeploymentTools +if sys.version_info < (3, 12): + import direct.p3d.AppRunner + import direct.p3d.DWBPackageInstaller + import direct.p3d.DeploymentTools + import direct.p3d.HostInfo + import direct.p3d.JavaScript + import direct.p3d.PackageInfo + import direct.p3d.PackageInstaller + import direct.p3d.PackageMerger + import direct.p3d.Packager import direct.p3d.FileSpec -import direct.p3d.HostInfo import direct.p3d.InstalledHostData import direct.p3d.InstalledPackageData -import direct.p3d.JavaScript -import direct.p3d.PackageInfo -import direct.p3d.PackageInstaller -import direct.p3d.PackageMerger -import direct.p3d.Packager import direct.p3d.PatchMaker import direct.p3d.ScanDirectoryNode import direct.p3d.SeqValue @@ -231,7 +236,8 @@ import direct.showbase.ShowBase import direct.showbase.TaskThreaded import direct.showbase.ThreeUpShow import direct.showbase.Transitions -import direct.showbase.VFSImporter +if sys.version_info < (3, 12): + import direct.showbase.VFSImporter import direct.showbase.WxGlobal import direct.showutil.BuildGeometry import direct.showutil.Effects diff --git a/tests/dist/test_FreezeTool.py b/tests/dist/test_FreezeTool.py new file mode 100644 index 0000000000..898c53e7e1 --- /dev/null +++ b/tests/dist/test_FreezeTool.py @@ -0,0 +1,64 @@ +from direct.dist.FreezeTool import Freezer, PandaModuleFinder +import sys + + +def test_Freezer_moduleSuffixes(): + freezer = Freezer() + + for suffix, mode, type in freezer.moduleSuffixes: + if type == 2: # imp.PY_SOURCE + assert mode == 'rb' + + +def test_Freezer_getModulePath_getModuleStar(tmp_path): + # Package 1 can be imported + package1 = tmp_path / "package1" + package1.mkdir() + (package1 / "submodule1.py").write_text("") + (package1 / "__init__.py").write_text("") + + # Package 2 can not be imported + package2 = tmp_path / "package2" + package2.mkdir() + (package2 / "submodule2.py").write_text("") + (package2 / "__init__.py").write_text("raise ImportError\n") + + # Module 1 can be imported + (tmp_path / "module1.py").write_text("") + + # Module 2 can not be imported + (tmp_path / "module2.py").write_text("raise ImportError\n") + + # Module 3 has a custom __path__ and __all__ + (tmp_path / "module3.py").write_text("__path__ = ['foobar']\n" + "__all__ = ['test']\n") + + backup = sys.path + try: + # Don't fail if first item on path does not exist + sys.path = [str(tmp_path / "nonexistent"), str(tmp_path)] + + freezer = Freezer() + assert freezer.getModulePath("nonexist") == None + assert freezer.getModulePath("package1") == [str(package1)] + assert freezer.getModulePath("package2") == [str(package2)] + assert freezer.getModulePath("package1.submodule1") == None + assert freezer.getModulePath("package1.nonexist") == None + assert freezer.getModulePath("package2.submodule2") == None + assert freezer.getModulePath("package2.nonexist") == None + assert freezer.getModulePath("module1") == None + assert freezer.getModulePath("module2") == None + assert freezer.getModulePath("module3") == ['foobar'] + + assert freezer.getModuleStar("nonexist") == None + assert freezer.getModuleStar("package1") == ['submodule1'] + assert freezer.getModuleStar("package2") == ['submodule2'] + assert freezer.getModuleStar("package1.submodule1") == None + assert freezer.getModuleStar("package1.nonexist") == None + assert freezer.getModuleStar("package2.submodule2") == None + assert freezer.getModuleStar("package2.nonexist") == None + assert freezer.getModuleStar("module1") == None + assert freezer.getModuleStar("module2") == None + assert freezer.getModuleStar("module3") == ['test'] + finally: + sys.path = backup From e4738194d50d433f9e4d1c5b59413459edc24f78 Mon Sep 17 00:00:00 2001 From: rdb Date: Fri, 13 Oct 2023 13:21:48 +0200 Subject: [PATCH 23/29] pfreeze: use clang, fix missing path with non-system Python on macOS --- direct/src/dist/FreezeTool.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/direct/src/dist/FreezeTool.py b/direct/src/dist/FreezeTool.py index 1ca9cf1c36..ce0aa1ad38 100644 --- a/direct/src/dist/FreezeTool.py +++ b/direct/src/dist/FreezeTool.py @@ -333,10 +333,15 @@ class CompilationEnvironment: self.arch = '-arch x86_64' elif proc in ('arm64', 'aarch64'): self.arch = '-arch arm64' - self.compileObjExe = "gcc -c %(arch)s -o %(basename)s.o -O2 -I%(pythonIPath)s %(filename)s" - self.compileObjDll = "gcc -fPIC -c %(arch)s -o %(basename)s.o -O2 -I%(pythonIPath)s %(filename)s" - self.linkExe = "gcc %(arch)s -o %(basename)s %(basename)s.o -framework Python" - self.linkDll = "gcc %(arch)s -undefined dynamic_lookup -bundle -o %(basename)s.so %(basename)s.o" + self.compileObjExe = "clang -c %(arch)s -o %(basename)s.o -O2 -I%(pythonIPath)s %(filename)s" + self.compileObjDll = "clang -fPIC -c %(arch)s -o %(basename)s.o -O2 -I%(pythonIPath)s %(filename)s" + self.linkExe = "clang %(arch)s -o %(basename)s %(basename)s.o" + if '/Python.framework/' in self.PythonIPath: + framework_dir = self.PythonIPath.split("/Python.framework/", 1)[0] + if framework_dir != "/System/Library/Frameworks": + self.linkExe += " -F " + framework_dir + self.linkExe += " -framework Python" + self.linkDll = "clang %(arch)s -undefined dynamic_lookup -bundle -o %(basename)s.so %(basename)s.o" else: # Unix From bf456baa35bd2213f7ae8b8adc27c341dc2c0756 Mon Sep 17 00:00:00 2001 From: rdb Date: Fri, 13 Oct 2023 13:35:40 +0200 Subject: [PATCH 24/29] workflow: Skip Python 3.12 tests on Windows for now Until we've added Python 3.12 to thirdparty packages --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c14a39546..64978eaf1c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,14 +33,17 @@ jobs: (cd thirdparty/darwin-libs-a && rm -rf rocket) - name: Set up Python 3.12 + if: matrix.os != 'windows-2019' uses: actions/setup-python@v4 with: python-version: '3.12' - name: Build Python 3.12 + if: matrix.os != 'windows-2019' shell: bash run: | python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 - name: Test Python 3.12 + if: matrix.os != 'windows-2019' shell: bash run: | python -m pip install pytest From 098fe634a5878802a30876853ed3e0d83c4b9fe4 Mon Sep 17 00:00:00 2001 From: WMOkiishi Date: Fri, 13 Oct 2023 13:20:20 -0600 Subject: [PATCH 25/29] task: Annotate core functions (#1548) --- direct/src/showbase/ShowBase.py | 2 +- direct/src/task/Task.py | 148 ++++++++++++++------- tests/task/test_Task.py | 224 +++++++++++++++++++++----------- 3 files changed, 249 insertions(+), 125 deletions(-) diff --git a/direct/src/showbase/ShowBase.py b/direct/src/showbase/ShowBase.py index 0a58cf74e9..75436a4d64 100644 --- a/direct/src/showbase/ShowBase.py +++ b/direct/src/showbase/ShowBase.py @@ -3421,7 +3421,7 @@ class ShowBase(DirectObject.DirectObject): # Set fWantTk to 0 to avoid starting Tk with this call self.startDirect(fWantDirect = fDirect, fWantTk = fTk, fWantWx = fWx) - def run(self): # pylint: disable=method-hidden + def run(self) -> None: # pylint: disable=method-hidden """This method runs the :class:`~direct.task.Task.TaskManager` when ``self.appRunner is None``, which is to say, when we are not running from within a p3d file. When we *are* within a p3d diff --git a/direct/src/task/Task.py b/direct/src/task/Task.py index b0aa610e40..9896b9c5ee 100644 --- a/direct/src/task/Task.py +++ b/direct/src/task/Task.py @@ -6,6 +6,8 @@ For more information about the task system, consult the :ref:`tasks-and-event-handling` page in the programming manual. """ +from __future__ import annotations + __all__ = ['Task', 'TaskManager', 'cont', 'done', 'again', 'pickup', 'exit', 'sequence', 'loop', 'pause'] @@ -13,7 +15,7 @@ __all__ = ['Task', 'TaskManager', from direct.directnotify.DirectNotifyGlobal import directNotify from direct.showbase.PythonUtil import Functor, ScratchPad from direct.showbase.MessengerGlobal import messenger -from typing import Any, Optional +from typing import Any, Callable, Coroutine, Final, Generator, Sequence, TypeVar, Union import types import random import importlib @@ -21,7 +23,7 @@ import sys # On Android, there's no use handling SIGINT, and in fact we can't, since we # run the application in a separate thread from the main thread. -signal: Optional[types.ModuleType] +signal: types.ModuleType | None if hasattr(sys, 'getandroidapilevel'): signal = None else: @@ -43,8 +45,15 @@ from panda3d.core import ( ) from direct.extensions_native import HTTPChannel_extensions # pylint: disable=unused-import +# The following variables are typing constructs used in annotations +# to succinctly express all the types that can be converted into tasks. +_T = TypeVar('_T', covariant=True) +_TaskCoroutine = Union[Coroutine[Any, None, _T], Generator[Any, None, _T]] +_TaskFunction = Callable[..., Union[int, _TaskCoroutine[Union[int, None]], None]] +_FuncOrTask = Union[_TaskFunction, _TaskCoroutine[Any], AsyncTask] -def print_exc_plus(): + +def print_exc_plus() -> None: """ Print the usual traceback information, followed by a listing of all the local variables in each frame. @@ -52,12 +61,13 @@ def print_exc_plus(): import traceback tb = sys.exc_info()[2] + assert tb is not None while 1: if not tb.tb_next: break tb = tb.tb_next stack = [] - f = tb.tb_frame + f: types.FrameType | None = tb.tb_frame while f: stack.append(f) f = f.f_back @@ -84,11 +94,11 @@ def print_exc_plus(): # these Python names, and define them both at the module level, here, # and at the class level (below). The preferred access is via the # class level. -done = AsyncTask.DSDone -cont = AsyncTask.DSCont -again = AsyncTask.DSAgain -pickup = AsyncTask.DSPickup -exit = AsyncTask.DSExit +done: Final = AsyncTask.DSDone +cont: Final = AsyncTask.DSCont +again: Final = AsyncTask.DSAgain +pickup: Final = AsyncTask.DSPickup +exit: Final = AsyncTask.DSExit #: Task aliases to :class:`panda3d.core.PythonTask` for historical purposes. Task = PythonTask @@ -112,7 +122,7 @@ gather = Task.gather shield = Task.shield -def sequence(*taskList): +def sequence(*taskList: AsyncTask) -> AsyncTaskSequence: seq = AsyncTaskSequence('sequence') for task in taskList: seq.addTask(task) @@ -122,7 +132,7 @@ def sequence(*taskList): Task.DtoolClassDict['sequence'] = staticmethod(sequence) -def loop(*taskList): +def loop(*taskList: AsyncTask) -> AsyncTaskSequence: seq = AsyncTaskSequence('loop') for task in taskList: seq.addTask(task) @@ -144,10 +154,10 @@ class TaskManager: __prevHandler: Any - def __init__(self): + def __init__(self) -> None: self.mgr = AsyncTaskManager.getGlobalPtr() - self.resumeFunc = None + self.resumeFunc: Callable[[], object] | None = None self.globalClock = self.mgr.getClock() self.stepping = False self.running = False @@ -157,12 +167,12 @@ class TaskManager: if signal: self.__prevHandler = signal.default_int_handler - self._frameProfileQueue = [] + self._frameProfileQueue: list[tuple[int, Any, Callable[[], object] | None]] = [] # this will be set when it's safe to import StateVar - self._profileFrames = None + self._profileFrames: Any = None self._frameProfiler = None - self._profileTasks = None + self._profileTasks: Any = None self._taskProfiler = None self._taskProfileInfo = ScratchPad( taskId = None, @@ -170,7 +180,7 @@ class TaskManager: session = None, ) - def finalInit(self): + def finalInit(self) -> None: # This function should be called once during startup, after # most things are imported. from direct.fsm.StatePush import StateVar @@ -179,7 +189,7 @@ class TaskManager: self._profileFrames = StateVar(False) self.setProfileFrames(ConfigVariableBool('profile-frames', 0).getValue()) - def destroy(self): + def destroy(self) -> None: # This should be safe to call multiple times. self.running = False self.notify.info("TaskManager.destroy()") @@ -187,10 +197,10 @@ class TaskManager: self._frameProfileQueue.clear() self.mgr.cleanup() - def __getClock(self): + def __getClock(self) -> ClockObject: return self.mgr.getClock() - def setClock(self, clockObject): + def setClock(self, clockObject: ClockObject) -> None: self.mgr.setClock(clockObject) self.globalClock = clockObject @@ -215,13 +225,13 @@ class TaskManager: # Next time around invoke the default handler signal.signal(signal.SIGINT, self.invokeDefaultHandler) - def getCurrentTask(self): + def getCurrentTask(self) -> AsyncTask | None: """ Returns the task currently executing on this thread, or None if this is being called outside of the task manager. """ return Thread.getCurrentThread().getCurrentTask() - def hasTaskChain(self, chainName): + def hasTaskChain(self, chainName: str) -> bool: """ Returns true if a task chain with the indicated name has already been defined, or false otherwise. Note that setupTaskChain() will implicitly define a task chain if it has @@ -231,9 +241,16 @@ class TaskManager: return self.mgr.findTaskChain(chainName) is not None - def setupTaskChain(self, chainName, numThreads = None, tickClock = None, - threadPriority = None, frameBudget = None, - frameSync = None, timeslicePriority = None): + def setupTaskChain( + self, + chainName: str, + numThreads: int | None = None, + tickClock: bool | None = None, + threadPriority: int | None = None, + frameBudget: float | None = None, + frameSync: bool | None = None, + timeslicePriority: bool | None = None, + ) -> None: """Defines a new task chain. Each task chain executes tasks potentially in parallel with all of the other task chains (if numThreads is more than zero). When a new task is created, it @@ -297,40 +314,50 @@ class TaskManager: if timeslicePriority is not None: chain.setTimeslicePriority(timeslicePriority) - def hasTaskNamed(self, taskName): + def hasTaskNamed(self, taskName: str) -> bool: """Returns true if there is at least one task, active or sleeping, with the indicated name. """ return bool(self.mgr.findTask(taskName)) - def getTasksNamed(self, taskName): + def getTasksNamed(self, taskName: str) -> list[AsyncTask]: """Returns a list of all tasks, active or sleeping, with the indicated name. """ return list(self.mgr.findTasks(taskName)) - def getTasksMatching(self, taskPattern): + def getTasksMatching(self, taskPattern: GlobPattern | str) -> list[AsyncTask]: """Returns a list of all tasks, active or sleeping, with a name that matches the pattern, which can include standard shell globbing characters like \\*, ?, and []. """ return list(self.mgr.findTasksMatching(GlobPattern(taskPattern))) - def getAllTasks(self): + def getAllTasks(self) -> list[AsyncTask]: """Returns list of all tasks, active and sleeping, in arbitrary order. """ return list(self.mgr.getTasks()) - def getTasks(self): + def getTasks(self) -> list[AsyncTask]: """Returns list of all active tasks in arbitrary order. """ return list(self.mgr.getActiveTasks()) - def getDoLaters(self): + def getDoLaters(self) -> list[AsyncTask]: """Returns list of all sleeping tasks in arbitrary order. """ return list(self.mgr.getSleepingTasks()) - def doMethodLater(self, delayTime, funcOrTask, name, extraArgs = None, - sort = None, priority = None, taskChain = None, - uponDeath = None, appendTask = False, owner = None): + def doMethodLater( + self, + delayTime: float, + funcOrTask: _FuncOrTask, + name: str | None, + extraArgs: Sequence | None = None, + sort: int | None = None, + priority: int | None = None, + taskChain: str | None = None, + uponDeath: Callable[[], object] | None = None, + appendTask: bool = False, + owner = None, + ) -> AsyncTask: """Adds a task to be performed at some time in the future. This is identical to `add()`, except that the specified delayTime is applied to the Task object first, which means @@ -353,9 +380,19 @@ class TaskManager: do_method_later = doMethodLater - def add(self, funcOrTask, name = None, sort = None, extraArgs = None, - priority = None, uponDeath = None, appendTask = False, - taskChain = None, owner = None, delay = None): + def add( + self, + funcOrTask: _FuncOrTask, + name: str | None = None, + sort: int | None = None, + extraArgs: Sequence | None = None, + priority: int | None = None, + uponDeath: Callable[[], object] | None = None, + appendTask: bool = False, + taskChain: str | None = None, + owner = None, + delay: float | None = None, + ) -> AsyncTask: """ Add a new task to the taskMgr. The task will begin executing immediately, or next frame if its sort value has already @@ -422,7 +459,18 @@ class TaskManager: self.mgr.add(task) return task - def __setupTask(self, funcOrTask, name, priority, sort, extraArgs, taskChain, appendTask, owner, uponDeath): + def __setupTask( + self, + funcOrTask: _FuncOrTask, + name: str | None, + priority: int | None, + sort: int | None, + extraArgs: Sequence | None, + taskChain: str | None, + appendTask: bool, + owner, + uponDeath: Callable[[], object] | None, + ) -> AsyncTask: wasTask = False if isinstance(funcOrTask, AsyncTask): task = funcOrTask @@ -480,7 +528,7 @@ class TaskManager: return task - def remove(self, taskOrName): + def remove(self, taskOrName: AsyncTask | str | list[AsyncTask | str]) -> int: """Removes a task from the task manager. The task is stopped, almost as if it had returned task.done. (But if the task is currently executing, it will finish out its current frame @@ -492,13 +540,15 @@ class TaskManager: if isinstance(taskOrName, AsyncTask): return self.mgr.remove(taskOrName) elif isinstance(taskOrName, list): + count = 0 for task in taskOrName: - self.remove(task) + count += self.remove(task) + return count else: tasks = self.mgr.findTasks(taskOrName) return self.mgr.remove(tasks) - def removeTasksMatching(self, taskPattern): + def removeTasksMatching(self, taskPattern: GlobPattern | str) -> int: """Removes all tasks whose names match the pattern, which can include standard shell globbing characters like \\*, ?, and []. See also :meth:`remove()`. @@ -508,7 +558,7 @@ class TaskManager: tasks = self.mgr.findTasksMatching(GlobPattern(taskPattern)) return self.mgr.remove(tasks) - def step(self): + def step(self) -> None: """Invokes the task manager for one frame, and then returns. Normally, this executes each task exactly once, though task chains that are in sub-threads or that have frame budgets @@ -519,7 +569,7 @@ class TaskManager: # Replace keyboard interrupt handler during task list processing # so we catch the keyboard interrupt but don't handle it until # after task list processing is complete. - self.fKeyboardInterrupt = 0 + self.fKeyboardInterrupt = False self.interruptCount = 0 if signal: @@ -541,7 +591,7 @@ class TaskManager: if self.fKeyboardInterrupt: raise KeyboardInterrupt - def run(self): + def run(self) -> None: """Starts the task manager running. Does not return until an exception is encountered (including KeyboardInterrupt). """ @@ -567,11 +617,11 @@ class TaskManager: if len(self._frameProfileQueue) > 0: numFrames, session, callback = self._frameProfileQueue.pop(0) - def _profileFunc(numFrames=numFrames): + def _profileFunc(numFrames: int = numFrames) -> None: self._doProfiledFrames(numFrames) session.setFunc(_profileFunc) session.run() - _profileFunc = None + del _profileFunc if callback: callback() session.release() @@ -624,7 +674,7 @@ class TaskManager: message = ioError return code, message - def stop(self): + def stop(self) -> None: # Set a flag so we will stop before beginning next frame self.running = False @@ -789,12 +839,12 @@ class TaskManager: task = tasks.getTask(i) return task - def __repr__(self): + def __repr__(self) -> str: return str(self.mgr) # In the event we want to do frame time managment, this is the # function to replace or overload. - def doYield(self, frameStartTime, nextScheduledTaskTime): + def doYield(self, frameStartTime: float, nextScheduledTaskTime: float) -> None: pass #def doYieldExample(self, frameStartTime, nextScheduledTaskTime): diff --git a/tests/task/test_Task.py b/tests/task/test_Task.py index 46710362b1..dbfaba3d28 100644 --- a/tests/task/test_Task.py +++ b/tests/task/test_Task.py @@ -1,22 +1,82 @@ +import pytest from panda3d import core from direct.task import Task -def test_TaskManager(): - tm = Task.TaskManager() - tm.mgr = core.AsyncTaskManager("Test manager") - tm.setClock(core.ClockObject()) - tm.setupTaskChain("default", tickClock = True) +TASK_NAME = 'Arbitrary task name' +TASK_CHAIN_NAME = 'Arbitrary task chain name' - tm._startTrackingMemLeaks = lambda: None - tm._stopTrackingMemLeaks = lambda: None - tm._checkMemLeaks = lambda: None - # check for memory leaks after every test - tm._startTrackingMemLeaks() - tm._checkMemLeaks() +def DUMMY_FUNCTION(*_): + pass + +@pytest.fixture +def task_manager(): + manager = Task.TaskManager() + manager.mgr = core.AsyncTaskManager('Test manager') + manager.clock = core.ClockObject() + manager.setupTaskChain('default', tickClock=True) + manager.finalInit() + yield manager + manager.destroy() + + +def test_sequence(task_manager): + numbers = [] + + def append_1(task): + numbers.append(1) + + def append_2(task): + numbers.append(2) + + sequence = Task.sequence(core.PythonTask(append_1), core.PythonTask(append_2)) + task_manager.add(sequence) + for _ in range(3): + task_manager.step() + assert not task_manager.getTasks() + assert numbers == [1, 2] + + +def test_loop(task_manager): + numbers = [] + + def append_1(task): + numbers.append(1) + + def append_2(task): + numbers.append(2) + + loop = Task.loop(core.PythonTask(append_1), core.PythonTask(append_2)) + task_manager.add(loop) + for _ in range(5): + task_manager.step() + assert numbers == [1, 2, 1, 2] + + +def test_get_current_task(task_manager): + def check_current_task(task): + assert task_manager.getCurrentTask().name == TASK_NAME + + task_manager.add(check_current_task, TASK_NAME) + assert len(task_manager.getTasks()) == 1 + assert task_manager.getCurrentTask() is None + + task_manager.step() + assert len(task_manager.getTasks()) == 0 + assert task_manager.getCurrentTask() is None + + +def test_has_task_chain(task_manager): + assert not task_manager.hasTaskChain(TASK_CHAIN_NAME) + task_manager.setupTaskChain(TASK_CHAIN_NAME) + assert task_manager.hasTaskChain(TASK_CHAIN_NAME) + + +def test_done(task_manager): # run-once task + tm = task_manager l = [] def _testDone(task, l=l): @@ -27,28 +87,31 @@ def test_TaskManager(): assert len(l) == 1 tm.step() assert len(l) == 1 - _testDone = None - tm._checkMemLeaks() + +def test_remove_by_name(task_manager): # remove by name + tm = task_manager def _testRemoveByName(task): return task.done tm.add(_testRemoveByName, 'testRemoveByName') assert tm.remove('testRemoveByName') == 1 assert tm.remove('testRemoveByName') == 0 - _testRemoveByName = None - tm._checkMemLeaks() + +def test_duplicate_named_tasks(task_manager): # duplicate named tasks + tm = task_manager def _testDupNamedTasks(task): return task.done tm.add(_testDupNamedTasks, 'testDupNamedTasks') tm.add(_testDupNamedTasks, 'testDupNamedTasks') assert tm.remove('testRemoveByName') == 0 - _testDupNamedTasks = None - tm._checkMemLeaks() + +def test_continued_task(task_manager): # continued task + tm = task_manager l = [] def _testCont(task, l = l): @@ -60,10 +123,11 @@ def test_TaskManager(): tm.step() assert len(l) == 2 tm.remove('testCont') - _testCont = None - tm._checkMemLeaks() + +def test_continue_until_done(task_manager): # continue until done task + tm = task_manager l = [] def _testContDone(task, l = l): @@ -80,20 +144,22 @@ def test_TaskManager(): tm.step() assert len(l) == 2 assert not tm.hasTaskNamed('testContDone') - _testContDone = None - tm._checkMemLeaks() + +def test_has_task_named(task_manager): # hasTaskNamed + tm = task_manager def _testHasTaskNamed(task): return task.done tm.add(_testHasTaskNamed, 'testHasTaskNamed') assert tm.hasTaskNamed('testHasTaskNamed') tm.step() assert not tm.hasTaskNamed('testHasTaskNamed') - _testHasTaskNamed = None - tm._checkMemLeaks() + +def test_task_sort(task_manager): # task sort + tm = task_manager l = [] def _testPri1(task, l = l): @@ -113,11 +179,11 @@ def test_TaskManager(): assert l == [1, 2, 1, 2,] tm.remove('testPri1') tm.remove('testPri2') - _testPri1 = None - _testPri2 = None - tm._checkMemLeaks() + +def test_extra_args(task_manager): # task extraArgs + tm = task_manager l = [] def _testExtraArgs(arg1, arg2, l=l): @@ -127,10 +193,11 @@ def test_TaskManager(): tm.step() assert len(l) == 2 assert l == [4, 5,] - _testExtraArgs = None - tm._checkMemLeaks() + +def test_append_task(task_manager): # task appendTask + tm = task_manager l = [] def _testAppendTask(arg1, arg2, task, l=l): @@ -140,10 +207,11 @@ def test_TaskManager(): tm.step() assert len(l) == 2 assert l == [4, 5,] - _testAppendTask = None - tm._checkMemLeaks() + +def test_task_upon_death(task_manager): # task uponDeath + tm = task_manager l = [] def _uponDeathFunc(task, l=l): @@ -155,11 +223,11 @@ def test_TaskManager(): tm.step() assert len(l) == 1 assert l == ['testUponDeath'] - _testUponDeath = None - _uponDeathFunc = None - tm._checkMemLeaks() + +def test_task_owner(task_manager): # task owner + tm = task_manager class _TaskOwner: def _addTask(self, task): self.addedTaskName = task.name @@ -175,11 +243,10 @@ def test_TaskManager(): tm.step() assert getattr(to, 'addedTaskName', None) == 'testOwner' assert getattr(to, 'clearedTaskName', None) == 'testOwner' - _testOwner = None - del to - _TaskOwner = None - tm._checkMemLeaks() + +def test_do_laters(task_manager): + tm = task_manager doLaterTests = [0,] # doLater @@ -205,8 +272,6 @@ def test_TaskManager(): _testDoLater1 = None _testDoLater2 = None _monitorDoLater = None - # don't check until all the doLaters are finished - #tm._checkMemLeaks() # doLater sort l = [] @@ -231,8 +296,6 @@ def test_TaskManager(): _testDoLaterPri1 = None _testDoLaterPri2 = None _monitorDoLaterPri = None - # don't check until all the doLaters are finished - #tm._checkMemLeaks() # doLater extraArgs l = [] @@ -252,8 +315,6 @@ def test_TaskManager(): tm.add(_monitorDoLaterExtraArgs, 'monitorDoLaterExtraArgs', sort=10) _testDoLaterExtraArgs = None _monitorDoLaterExtraArgs = None - # don't check until all the doLaters are finished - #tm._checkMemLeaks() # doLater appendTask l = [] @@ -275,8 +336,6 @@ def test_TaskManager(): tm.add(_monitorDoLaterAppendTask, 'monitorDoLaterAppendTask', sort=10) _testDoLaterAppendTask = None _monitorDoLaterAppendTask = None - # don't check until all the doLaters are finished - #tm._checkMemLeaks() # doLater uponDeath l = [] @@ -302,8 +361,6 @@ def test_TaskManager(): _testUponDeathFunc = None _testDoLaterUponDeath = None _monitorDoLaterUponDeath = None - # don't check until all the doLaters are finished - #tm._checkMemLeaks() # doLater owner class _DoLaterOwner: @@ -335,15 +392,15 @@ def test_TaskManager(): _monitorDoLaterOwner = None del doLaterOwner _DoLaterOwner = None - # don't check until all the doLaters are finished - #tm._checkMemLeaks() # run the doLater tests while doLaterTests[0] > 0: tm.step() del doLaterTests - tm._checkMemLeaks() + +def test_get_tasks(task_manager): + tm = task_manager # getTasks def _testGetTasks(task): return task.cont @@ -361,9 +418,10 @@ def test_TaskManager(): tm.remove('testGetTasks1') tm.remove('testGetTasks3') assert len(tm.getTasks()) == 0 - _testGetTasks = None - tm._checkMemLeaks() + +def test_get_do_laters(task_manager): + tm = task_manager # getDoLaters def _testGetDoLaters(): pass @@ -379,9 +437,18 @@ def test_TaskManager(): tm.remove('testDoLater1') tm.remove('testDoLater3') assert len(tm.getDoLaters()) == 0 - _testGetDoLaters = None - tm._checkMemLeaks() + +def test_get_all_tasks(task_manager): + active_task = task_manager.add(DUMMY_FUNCTION, delay=None) + sleeping_task = task_manager.add(DUMMY_FUNCTION, delay=1) + assert task_manager.getTasks() == [active_task] + assert task_manager.getDoLaters() == [sleeping_task] + assert task_manager.getAllTasks() in ([active_task, sleeping_task], [sleeping_task, active_task]) + + +def test_duplicate_named_do_laters(task_manager): + tm = task_manager # duplicate named doLaters removed via taskMgr.remove def _testDupNameDoLaters(): pass @@ -391,9 +458,10 @@ def test_TaskManager(): assert len(tm.getDoLaters()) == 2 tm.remove('testDupNameDoLater') assert len(tm.getDoLaters()) == 0 - _testDupNameDoLaters = None - tm._checkMemLeaks() + +def test_duplicate_named_do_laters_remove(task_manager): + tm = task_manager # duplicate named doLaters removed via remove() def _testDupNameDoLatersRemove(): pass @@ -405,10 +473,10 @@ def test_TaskManager(): assert len(tm.getDoLaters()) == 1 dl1.remove() assert len(tm.getDoLaters()) == 0 - _testDupNameDoLatersRemove = None - # nameDict etc. isn't cleared out right away with task.remove() - tm._checkMemLeaks() + +def test_get_tasks_named(task_manager): + tm = task_manager # getTasksNamed def _testGetTasksNamed(task): return task.cont @@ -421,9 +489,20 @@ def test_TaskManager(): assert len(tm.getTasksNamed('testGetTasksNamed')) == 3 tm.remove('testGetTasksNamed') assert len(tm.getTasksNamed('testGetTasksNamed')) == 0 - _testGetTasksNamed = None - tm._checkMemLeaks() + +def test_get_tasks_matching(task_manager): + task_manager.add(DUMMY_FUNCTION, 'task_1') + task_manager.add(DUMMY_FUNCTION, 'task_2') + task_manager.add(DUMMY_FUNCTION, 'another_task') + + assert len(task_manager.getTasksMatching('task_?')) == 2 + assert len(task_manager.getTasksMatching('*_task')) == 1 + assert len(task_manager.getTasksMatching('*task*')) == 3 + + +def test_remove_tasks_matching(task_manager): + tm = task_manager # removeTasksMatching def _testRemoveTasksMatching(task): return task.cont @@ -445,9 +524,10 @@ def test_TaskManager(): tm.removeTasksMatching('testRemoveTasksMatching?a') assert len(tm.getTasksNamed('testRemoveTasksMatching1a')) == 0 assert len(tm.getTasksNamed('testRemoveTasksMatching2a')) == 0 - _testRemoveTasksMatching = None - tm._checkMemLeaks() + +def test_task_obj(task_manager): + tm = task_manager # create Task object and add to mgr l = [] @@ -463,9 +543,10 @@ def test_TaskManager(): tm.remove('testTaskObj') tm.step() assert len(l) == 2 - _testTaskObj = None - tm._checkMemLeaks() + +def test_task_remove(task_manager): + tm = task_manager # remove Task via task.remove() l = [] @@ -482,9 +563,10 @@ def test_TaskManager(): tm.step() assert len(l) == 2 del t - _testTaskObjRemove = None - tm._checkMemLeaks() + +def test_task_get_sort(task_manager): + tm = task_manager # set/get Task sort l = [] def _testTaskObjSort(arg, task, l=l): @@ -508,11 +590,3 @@ def test_TaskManager(): t2.remove() tm.step() assert len(l) == 4 - del t1 - del t2 - _testTaskObjSort = None - tm._checkMemLeaks() - - del l - tm.destroy() - del tm From c45e14a5638d4db055d5f35845e3dba7a4e774cf Mon Sep 17 00:00:00 2001 From: rdb Date: Fri, 13 Oct 2023 21:21:29 +0200 Subject: [PATCH 26/29] interrogatedb: Switch T_OBJECT to T_OBJECT_EX for this_metatype T_OBJECT has been deprecated. --- dtool/src/interrogatedb/dtool_super_base.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dtool/src/interrogatedb/dtool_super_base.cxx b/dtool/src/interrogatedb/dtool_super_base.cxx index 0992edb08f..085830a211 100644 --- a/dtool/src/interrogatedb/dtool_super_base.cxx +++ b/dtool/src/interrogatedb/dtool_super_base.cxx @@ -21,7 +21,7 @@ static PyMemberDef standard_type_members[] = { {(char *)"this_const", T_BOOL, offsetof(Dtool_PyInstDef, _is_const), READONLY, (char *)"C++ 'this' const flag"}, // {(char *)"this_signature", T_INT, offsetof(Dtool_PyInstDef, _signature), // READONLY, (char *)"A type check signature"}, - {(char *)"this_metatype", T_OBJECT, offsetof(Dtool_PyInstDef, _My_Type), READONLY, (char *)"The dtool meta object"}, + {(char *)"this_metatype", T_OBJECT_EX, offsetof(Dtool_PyInstDef, _My_Type), READONLY, (char *)"The dtool meta object"}, {nullptr} /* Sentinel */ }; From 5b041474ff54be8ce296ce0d2317f37af8494be9 Mon Sep 17 00:00:00 2001 From: rdb Date: Fri, 13 Oct 2023 23:29:21 +0200 Subject: [PATCH 27/29] glgsg: Fix wrong value for deprecated shadowMatrix shader input --- panda/src/glstuff/glShaderContext_src.cxx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/panda/src/glstuff/glShaderContext_src.cxx b/panda/src/glstuff/glShaderContext_src.cxx index 77bba57166..f3a3667825 100644 --- a/panda/src/glstuff/glShaderContext_src.cxx +++ b/panda/src/glstuff/glShaderContext_src.cxx @@ -1570,8 +1570,8 @@ reflect_uniform(int i, char *name_buffer, GLsizei name_buflen) { bind._func = Shader::SMF_compose; bind._part[0] = Shader::SMO_model_to_apiview; bind._arg[0] = nullptr; - bind._part[1] = Shader::SMO_apiview_to_apiclip_light_source_i; - bind._arg[1] = nullptr; + bind._part[1] = Shader::SMO_mat_constant_x_attrib; + bind._arg[1] = iname->get_parent()->append("shadowViewMatrix"); } else { bind._part[0] = Shader::SMO_mat_constant_x_attrib; bind._arg[0] = InternalName::make(param_name); From e627c7c63d90a979373b0a41fd9e1bda4e79319b Mon Sep 17 00:00:00 2001 From: rdb Date: Fri, 13 Oct 2023 23:30:21 +0200 Subject: [PATCH 28/29] particles: Fix deprecation warning --- direct/src/particles/ParticleEffect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/direct/src/particles/ParticleEffect.py b/direct/src/particles/ParticleEffect.py index 96f544d1d4..888fda216f 100644 --- a/direct/src/particles/ParticleEffect.py +++ b/direct/src/particles/ParticleEffect.py @@ -97,7 +97,7 @@ class ParticleEffect(NodePath): def addForceGroup(self, forceGroup): forceGroup.nodePath.reparentTo(self) forceGroup.particleEffect = self - self.forceGroupDict[forceGroup.getName()] = forceGroup + self.forceGroupDict[forceGroup.name] = forceGroup # Associate the force group with all particles for force in forceGroup: From 6f89b8e72090c4ab4302dbe357b8d005cf60a847 Mon Sep 17 00:00:00 2001 From: rdb Date: Fri, 13 Oct 2023 23:32:48 +0200 Subject: [PATCH 29/29] makepanda: Remove stray distutils import --- makepanda/makepandacore.py | 1 - 1 file changed, 1 deletion(-) diff --git a/makepanda/makepandacore.py b/makepanda/makepandacore.py index 0e49a28e02..ee4af18d0d 100644 --- a/makepanda/makepandacore.py +++ b/makepanda/makepandacore.py @@ -6,7 +6,6 @@ ######################################################################## import configparser -from distutils import sysconfig # DO NOT CHANGE to sysconfig - see #1230 import fnmatch import getpass import glob