diff --git a/.github/workflows/checks-workflow.yml b/.github/workflows/checks-workflow.yml index 05e4038..4bdf2f3 100644 --- a/.github/workflows/checks-workflow.yml +++ b/.github/workflows/checks-workflow.yml @@ -11,5 +11,5 @@ jobs: # this is a mega hack, to be fixed run: | sudo apt-get install pylint3 dos2unix - chmod +x ./checks.sh - ./checks.sh + chmod +x ./scripts/checks.sh + ./scripts/checks.sh diff --git a/.pylintrc b/.pylintrc index 4752f7c..cfa5d15 100644 --- a/.pylintrc +++ b/.pylintrc @@ -433,7 +433,7 @@ ignored-classes=optparse.Values,thread._local,_thread._local # (useful for modules/projects where namespaces are manipulated during runtime # and thus existing member attributes cannot be deduced by static analysis). It # supports qualified module names, as well as Unix pattern matching. -ignored-modules=pymxs,ptvsd,PySide2 +ignored-modules=pymxs,ptvsd,PySide2,menuhook # Show a hint with possible names when a member name was not found. The aspect # of finding the hint is based on edit distance. diff --git a/README.md b/README.md index 41be460..c4a91bf 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # How To - Practical Examples For Python -![Splash](Splash.png) +![Splash](/doc/Splash.png) This repo contains various Python programming samples. @@ -15,133 +15,67 @@ practice to package 3ds Max Python tools with pip, we provide all our examples i It is not necessary to install the HowTos: the repo can simply be used as a passive directory of samples and documentation for Python developers. -This being said, it is also possible to install the samples in 3ds Max. This -will add a Python3 scripting menu to 3ds Max: +- Installing the HowTos will add menu items to 3ds Max, and is documented [here](doc/install.md) +- After an update from github it is necessary to rerun install scripts to get everything +working as expected -![Integration](Integration.png) - -The examples and some development goodies will be made available from there. - -The installation does the following: -- it installs pip in your 3ds Max installation if it's not already there -- it installs pystartup.ms that enables auto start pip packages -- it installs all the samples in --user and -e mode with pip - -If you decide to install the howtos, it is highly recommended that you clone -this git repository locally using git bash (whenever we update the samples, -you will be able to update your local version and re-run the installation scripts): - -```bash -# from the directory where you want the sample -git clone https://github.com/ADN-DevTech/3dsMax-Python-HowTos.git -``` - -Also note that *all installation steps decribed here also use git bash* (it is -possible to use another client for git but all installation scripts in -this repo use bash). - -### Option A: Install Everthing Locally in One Step (--user) -> Note: the steps described here need to be done from a git bash prompt - -The [install.sh](install.sh) script can be used from bash -to install the samples in 3ds Max. The script needs to be run from a -3ds Max installation directory. - -### Option B: Install Everything Locally in Two Steps (--user) -> Note: the steps described here need to be done from a git bash prompt +## Python How Tos -It is possible to break up the installation in two steps. +The samples below are translations of [MAXScript How Tos](https://help.autodesk.com/view/MAXDEV/2021/ENU/?guid=GUID-25C9AD58-3665-471E-8B4B-54A094C1D5C9) that +can be found in the 3ds Max online documentation. -- The [installstartup.sh](installstartup.sh) script can be used -from bash to install pip and [pystartup.ms](/pystartup/pystartup.ms). -It needs to run in the 3ds Max installation directory. +The conversion from MaxScript to Python could have been more mechanical but we chose to implement +the Python version in the best Python way known to us. An example of this is that we use PySide2 +(Qt) for the UI as much as possible instead of using more traditional 3ds Max ui mechanisms. -You may do only this step if you don't want the HowTos but you -want to install pip and pystartup.ms. +*How To?* -- The [installhowtos.sh](installhowtos.sh) script can be used from -bash to pip install all the howtos in `--user` mode and `-e` mode (--user -means that the samples will be intalled under `~/AppData/Roaming/Python/Python37/site-packages/`, -and -e means that the packages will be installed as symlinks to the -source directories so that if the sources change the packages don't need -to be reinstalled). -This script needs to run in the 3ds Max installation directory. +- Develop a Transform Lock Script [transformlock](/src/packages/transformlock/README.md) +- Remove all materials [removeallmaterials](/src/packages/removeallmaterials/README.md) +- Quickly rename selected objects [renameselected](/src/packages/renameselected/README.md) +- Output Object Data to File [speedsheet](/src/packages/speedsheet/README.md) +- Create a quick video preview [quickpreview](/src/packages/quickpreview/README.md) +- Access the Z-Depth Channel [zdepthchannel](/src/packages/zdepthchannel/README.md) -## Uninstalling the HowTos +## Python Examples that don't come from maxscript howtos -The steps needed to uninstall the HowTos can be found in [uninstall.md](uninstall.md). +- Update a progressbar from a Python thread [threadprogressbar](/src/packages/threadprogressbar/README.md) +- Create a single instance modal dialog [singleinstancedlg](/src/packages/singleinstancedlg/README.md) +- Add menu items to open documentation pages in the web browser [inbrowserhelp](/src/packages/inbrowserhelp/README.md) -### Option C: Install the howtos in a virtual environment -> Note: the steps described here need to be done from a git bash prompt +## Python Samples -This last option requires three steps. +Python samples can be found in [src/samples](/src/samples). These samples may already be in your 3ds Max +installation directories. -It can be used to install the HowTos in a virtual environment (ex: -you may want to have a virtual environment for Python development). +## 3dsMax startup entry point -- The first step is the same as the first step described in option B: -[installstartup.sh](installstartup.sh) needs to run in the 3ds Max -installation directory to install pip if it is missing and pystartup. +[pystartup](/src/pystartup/README.md) provides the maxscript code that, when copied to 3ds Max's +startup directory, will automatically launch pip packages with the 3dsMax startup +entry point. -- The second step consists in installing virtualenv with pip and creating a -virtual environment. These steps are described in the [3ds Max documentation](http://help.autodesk.com/view/MAXDEV/2021/ENU/?guid=__developer_python_3_support_virtual_env_html). -- The last step consists in installing the HowTos in the virtual environment. -From the same git bash prompt, the [installhowtos.sh](/installhowtos.sh) -script can be used to install the HowTos in a virtual environment. First `cd` -to the directory of the virtual env and then (without activating the env) simply -run [installhowtos.sh](/installhowtos.sh) from that directory. +## Tools -## Packages that are not examples but that are provided in this repo +The following packages are not really examples but python tools. -- [menuhook](menuhook/README.md) is not meant to be an example (but is still interesting as such!) but +- [menuhook](/src/packages/menuhook/README.md) is not meant to be an example (but is still interesting as such!) but as a way of attaching Python functions to 3ds Max menu items. The menuhook package is used by most of the other samples. -- [realoadmod](reloadmod/README.md) is small tool that will reload all development modules in one +- [realoadmod](/src/packages/reloadmod/README.md) is small tool that will reload all development modules in one operation -- [mxvscode](mxvscode/README.md) is a small tool that will automatically import ptvsd (the +- [mxvscode](/src/packages/mxvscode/README.md) is a small tool that will automatically import ptvsd (the VSCode debugging interface) during the startup of 3ds Max and make it accept remote connections. This may slow down the startup of 3ds Max quite a bit and is meant as a developer-only tool. - -## Python How Tos - -The samples below are translations of [MAXScript How Tos](https://help.autodesk.com/view/3DSMAX/2020/ENU/?guid=GUID-25C9AD58-3665-471E-8B4B-54A094C1D5C9) that -can be found in the 3ds Max online documentation. - -The conversion from MaxScript to Python could have been more mechanical but we chose to implement -the Python version in the best Python way known to us. An example of this is that we use PySide2 -(Qt) for the UI as much as possible instead of using more traditional 3ds Max ui mechanisms. - -*How To?* - -- Develop a Transform Lock Script [transformlock](transformlock/README.md) -- Remove all materials [removeallmaterials](removeallmaterials/README.md) -- Quickly rename selected objects [renameselected](renameselected/README.md) -- Output Object Data to File [speedsheet](speedsheet/README.md) -- Create a quick video preview [quickpreview](quickpreview/README.md) -- Access the Z-Depth Channel [zdepthchannel](zdepthchannel/README.md) - -## Python Examples that don't come from maxscript howtos - -- Update a progressbar from a Python thread [threadprogressbar](threadprogressbar/README.md) -- Create a single instance modal dialog [singleinstancedlg](singleinstancedlg/README.md) -- Add menu items to open documentation pages in the web browser [inbrowserhelp](inbrowserhelp/README.md) - -## 3dsMax startup entry point - -[pystartup](pystartup/README.md) provides the maxscript code that, when copied to 3ds Max's -startup directory, will automatically launch pip packages with the 3dsMax startup -entry point. - ## Extra Goodies -- [create.sh](create.sh) will generate an empty pip package in the current working directory. - [install.sh](install.sh) will install pip, install pystartup and pip install all the samples - [uninstall.sh](uninstall.sh) will uninstall what was installed with install.sh - [installstartup.sh](installstartup.sh) will install pip and pystartup and nothing more - [installhowtos.sh](installhowtos.sh) will install only the howtos (works in a virtual env) -- [checks.sh](checks.sh) runs pylint on the code, validates that 3ds Max is named properly, +- [checks.sh](/scripts/checks.sh) runs pylint on the code, validates that 3ds Max is named properly, validates that code blocks in markdown always specify the programming language, checks that all links are valid in all markdown files of the repo +- [create.sh](/scripts/create.sh) will generate an empty pip package in the current working directory. diff --git a/Integration.png b/doc/Integration.png similarity index 100% rename from Integration.png rename to doc/Integration.png diff --git a/Splash.png b/doc/Splash.png similarity index 100% rename from Splash.png rename to doc/Splash.png diff --git a/doc/install.md b/doc/install.md new file mode 100644 index 0000000..98a0801 --- /dev/null +++ b/doc/install.md @@ -0,0 +1,79 @@ +# Installation + +It is possible to install the samples in 3ds Max. This +will add a Python3 scripting menu to 3ds Max: + +![Integration](/doc/Integration.png) + +The examples and some development goodies will be made available from there. + +The installation does the following: +- it installs pip in your 3ds Max installation if it's not already there +- it installs pystartup.ms that enables auto start pip packages +- it installs all the samples in --user and -e mode with pip + +If you decide to install the howtos, it is highly recommended that you clone +this git repository locally using git bash (whenever we update the samples, +you will be able to update your local version and re-run the installation scripts): + +```bash +# from the directory where you want the sample +git clone https://github.com/ADN-DevTech/3dsMax-Python-HowTos.git +``` + +Also note that *all installation steps decribed here also use git bash* (it is +possible to use another client for git but all installation scripts in +this repo use bash). + +### Option A: Install Everthing Locally in One Step (--user) +> Note: the steps described here need to be done from a git bash prompt + +The [install.sh](/install.sh) script can be used from bash +to install the samples in 3ds Max. The script needs to be run from a +3ds Max installation directory. + +### Option B: Install Everything Locally in Two Steps (--user) +> Note: the steps described here need to be done from a git bash prompt + +It is possible to break up the installation in two steps. + +- The [installstartup.sh](/installstartup.sh) script can be used +from bash to install pip and [pystartup.ms](/src/pystartup/pystartup.ms). +It needs to run in the 3ds Max installation directory. + +You may do only this step if you don't want the HowTos but you +want to install pip and pystartup.ms. + +- The [installhowtos.sh](/installhowtos.sh) script can be used from +bash to pip install all the howtos in `--user` mode and `-e` mode (--user +means that the samples will be intalled under `~/AppData/Roaming/Python/Python37/site-packages/`, +and -e means that the packages will be installed as symlinks to the +source directories so that if the sources change the packages don't need +to be reinstalled). +This script needs to run in the 3ds Max installation directory. + +## Uninstalling the HowTos + +The steps needed to uninstall the HowTos can be found in [uninstall.md](/doc/uninstall.md). + +### Option C: Install the howtos in a virtual environment +> Note: the steps described here need to be done from a git bash prompt + +This last option requires three steps. + +It can be used to install the HowTos in a virtual environment (ex: +you may want to have a virtual environment for Python development). + +- The first step is the same as the first step described in option B: +[installstartup.sh](/installstartup.sh) needs to run in the 3ds Max +installation directory to install pip if it is missing and pystartup. + +- The second step consists in installing virtualenv with pip and creating a +virtual environment. These steps are described in the [3ds Max documentation](http://help.autodesk.com/view/MAXDEV/2021/ENU/?guid=__developer_python_3_support_virtual_env_html). +- The last step consists in installing the HowTos in the virtual environment. +From the same git bash prompt, the [installhowtos.sh](/installhowtos.sh) +script can be used to install the HowTos in a virtual environment. First `cd` +to the directory of the virtual env and then (without activating the env) simply +run [installhowtos.sh](/installhowtos.sh) from that directory. + + diff --git a/uninstall.md b/doc/uninstall.md similarity index 97% rename from uninstall.md rename to doc/uninstall.md index afe66b5..460fa93 100644 --- a/uninstall.md +++ b/doc/uninstall.md @@ -55,7 +55,7 @@ The HowTos can be uninstalled individually by calling: ## Uninstalling all the HowTos at Once (manual uinstall) -The [uninstallhowtos.sh](uninstallhowtos.sh) can be used +The [uninstallhowtos.sh](/uninstallhowtos.sh) can be used to uninstall all the howtos at once. This will automatically call `./python.exe -m pip uninstall` for all the HowTos packages. diff --git a/install.sh b/install.sh index 7670e01..91d04ba 100644 --- a/install.sh +++ b/install.sh @@ -1,7 +1,7 @@ #! /usr/bin/env bash set -e script=$(dirname $(readlink -f "$0")) -source "$script/inst.sh" +source "$script/scripts/inst.sh" # make sure we have 3ds Max in the current path if [ ! -f ./3dsmax.exe ] diff --git a/installhowtos.sh b/installhowtos.sh index 8e2dd4c..3b5bcfb 100644 --- a/installhowtos.sh +++ b/installhowtos.sh @@ -1,7 +1,7 @@ #! /usr/bin/env bash set -e script=$(dirname $(readlink -f "$0")) -source "$script/inst.sh" +source "$script/scripts/inst.sh" # make sure cygpath is available if ! command -v cygpath >/dev/null 2>&1 @@ -18,7 +18,7 @@ fi venvscript () { echo "cd Scripts" echo "call activate.bat" - for f in $(find "$script" -name "setup.py") + for f in $(find "$packagedir" -name "setup.py") do local package=$(dirname "$f") echo "pip.exe install -e \"$(cygpath -d "$package")\"" diff --git a/installstartup.sh b/installstartup.sh index cf41613..30a1955 100644 --- a/installstartup.sh +++ b/installstartup.sh @@ -1,7 +1,7 @@ #! /usr/bin/env bash set -e script=$(dirname $(readlink -f "$0")) -source "$script/inst.sh" +source "$script/scripts/inst.sh" # make sure we have 3ds Max in the current path if [ ! -f ./3dsmax.exe ] diff --git a/checks.sh b/scripts/checks.sh similarity index 74% rename from checks.sh rename to scripts/checks.sh index 7429315..4269e61 100644 --- a/checks.sh +++ b/scripts/checks.sh @@ -1,24 +1,35 @@ #! /usr/bin/env bash set -e script=$(dirname $(readlink -f "$0")) +packagedir="$script/../src/packages" workdir=$(pwd) IFS=$'\n' +pl=pylint3 +lintdir() { + comment="$1" + folder="$2" + echo "$comment" + $pl "$folder" + # also prevent runtime.execute + if grep -n -R -E "runtime\.execute\(|rt.execute\(" --include '*.py' "$folder" + then + echo "pymxs.execute used" + exit 1 + fi +} lint() { - for f in $(find . -name "setup.py") + for f in $(find "$packagedir" -name "setup.py") do local package=$(basename $(dirname "$f")) - echo "$package" - pylint3 "./$package/$package" - # also prevent runtime.execute - if grep -n -R -E "runtime\.execute\(|rt.execute\(" --include '*.py' "./$package/$package" - then - echo "pymxs.execute used" - exit 1 - fi + lintdir "$package" "$packagedir/$package/$package" done } +lintsamples() { + lintdir "samples" "$script/../src/samples" +} + checkmarkdown() { # find code blocks in markdown that don't specify the language git grep -n '```' -- "*.md" | @@ -50,12 +61,12 @@ checkmdlinks() { fi elif [[ "$url" =~ ^/.* ]] then - if [ ! -f "$workdir$url" ] + if [ ! -e "$workdir$url" ] then echo "$file$line: Broken absolute link: $url" fi else - if [ ! -f "$filedir/$url" ] + if [ ! -e "$filedir/$url" ] then echo "$file:$line: Broken relative link: $url" fi @@ -73,5 +84,6 @@ checkmarkdownlinks() { lint +lintsamples checkmarkdown checkmarkdownlinks diff --git a/create.sh b/scripts/create.sh similarity index 100% rename from create.sh rename to scripts/create.sh diff --git a/inst.sh b/scripts/inst.sh similarity index 88% rename from inst.sh rename to scripts/inst.sh index a7673ba..9652019 100644 --- a/inst.sh +++ b/scripts/inst.sh @@ -1,6 +1,7 @@ set -e script=$(dirname $(readlink -f "$0")) installdir=$(pwd) +packagedir="$script/src/packages" if [ ! -f "$installdir/installSettings.ini" ] then @@ -39,13 +40,13 @@ installpip() { # install pystartup.ms installpystartup() { - cp "$script/pystartup/pystartup.ms" "$startuppath" + cp "$script/src/pystartup/pystartup.ms" "$startuppath" } # install all Python packages in the repo with the -e option installpythonpackages() { - for f in $(find "$script" -name "setup.py") + for f in $(find "$packagedir" -name "setup.py") do local package=$(dirname "$f") "$installdir/Python37/python.exe" -m pip install --user -e "$package" @@ -54,7 +55,7 @@ installpythonpackages() { # uninstall all Python packages in the repo uninstallpythonpackages() { - for f in $(find "$script" -name "setup.py") + for f in $(find "$packagedir" -name "setup.py") do local package=$(basename "$(dirname "$f")") local pname="$package-autodesk" diff --git a/makepreview.sh b/scripts/makepreview.sh similarity index 100% rename from makepreview.sh rename to scripts/makepreview.sh diff --git a/inbrowserhelp/LICENSE b/src/packages/inbrowserhelp/LICENSE similarity index 100% rename from inbrowserhelp/LICENSE rename to src/packages/inbrowserhelp/LICENSE diff --git a/inbrowserhelp/README.md b/src/packages/inbrowserhelp/README.md similarity index 100% rename from inbrowserhelp/README.md rename to src/packages/inbrowserhelp/README.md diff --git a/inbrowserhelp/inbrowserhelp/__init__.py b/src/packages/inbrowserhelp/inbrowserhelp/__init__.py similarity index 90% rename from inbrowserhelp/inbrowserhelp/__init__.py rename to src/packages/inbrowserhelp/inbrowserhelp/__init__.py index 7defecb..374aa17 100644 --- a/inbrowserhelp/inbrowserhelp/__init__.py +++ b/src/packages/inbrowserhelp/inbrowserhelp/__init__.py @@ -9,6 +9,8 @@ "help.autodesk.com/view/MAXDEV/2021/ENU/?guid=__developer_tutorials_creating_the_dialog_html"), ("howtos", "Python HowTos Github Repo", "github.com/ADN-DevTech/3dsMax-Python-HowTos"), + ("samples", "Python samples (Github Repo)", + "github.com/ADN-DevTech/3dsMax-Python-HowTos/src/samples"), ("pymxs", "Pymxs Online Documentation", "help.autodesk.com/view/MAXDEV/2021/ENU/?guid=__developer_using_pymxs_html"), ("pyside2", "Qt for Python Documentation (PySide2)", diff --git a/inbrowserhelp/setup.py b/src/packages/inbrowserhelp/setup.py similarity index 100% rename from inbrowserhelp/setup.py rename to src/packages/inbrowserhelp/setup.py diff --git a/menuhook/LICENSE b/src/packages/menuhook/LICENSE similarity index 100% rename from menuhook/LICENSE rename to src/packages/menuhook/LICENSE diff --git a/menuhook/README.md b/src/packages/menuhook/README.md similarity index 93% rename from menuhook/README.md rename to src/packages/menuhook/README.md index 2e01b31..3fdb121 100644 --- a/menuhook/README.md +++ b/src/packages/menuhook/README.md @@ -77,7 +77,7 @@ change in the future. *Q:* Doing this creates a 'macro' and this macro never goes away *A:* The Python & maxscript apis currently do not allow to remove macros -that have been created. So the [macros](https://help.autodesk.com/view/3DSMAX/2020/ENU/?guid=GUID-3DC75DDE-E4BC-4033-ABA9-A42063036CB9) +that have been created. So the [macros](https://help.autodesk.com/view/MAXDEV/2021/ENU/?guid=GUID-3DC75DDE-E4BC-4033-ABA9-A42063036CB9) that we create are permanent but if we don't load the Python packages that implement them during an execution of 3ds Max they become dangling. We are able to detect that @@ -86,7 +86,7 @@ and notify the users. *Q:* Would it be possible to remove menu items (to make them not permanent)? -*A:* The [menu manager](https://help.autodesk.com/view/3DSMAX/2020/ENU/?guid=GUID-258F6015-6B45-4A87-A7F5-BB091A2AE065), +*A:* The [menu manager](https://help.autodesk.com/view/MAXDEV/2021/ENU/?guid=GUID-258F6015-6B45-4A87-A7F5-BB091A2AE065), provides a way to remove menu items. This could be used. But the solution would not be crash proof (it would be difficult to establish that the menu item has been removed for real, by using only what the menu manager provides). @@ -104,7 +104,7 @@ from pymxs import runtime as rt To get access to the 3dsMax scripting library for Python (pymxs). -Then it uses `rt.macros` to access functions from the [macro scripts](https://help.autodesk.com/view/3DSMAX/2020/ENU/?guid=GUID-3DC75DDE-E4BC-4033-ABA9-A42063036CB9). +Then it uses `rt.macros` to access functions from the [macro scripts](https://help.autodesk.com/view/MAXDEV/2021/ENU/?guid=GUID-3DC75DDE-E4BC-4033-ABA9-A42063036CB9). As it is now we have to create a macro for a function we want to hook to the menu. The macro has an action and a category (that identifies it). It is done by this call: @@ -118,7 +118,7 @@ The macro has an action and a category (that identifies it). It is done by this mxs) ``` -When this (category, action) exists, we can use `rt.menuman`, the [menu manager](https://help.autodesk.com/view/3DSMAX/2020/ENU/?guid=GUID-258F6015-6B45-4A87-A7F5-BB091A2AE065) +When this (category, action) exists, we can use `rt.menuman`, the [menu manager](https://help.autodesk.com/view/MAXDEV/2021/ENU/?guid=GUID-258F6015-6B45-4A87-A7F5-BB091A2AE065) to find the menu in which we want to add an item: ```python diff --git a/menuhook/menuhook/__init__.py b/src/packages/menuhook/menuhook/__init__.py similarity index 100% rename from menuhook/menuhook/__init__.py rename to src/packages/menuhook/menuhook/__init__.py diff --git a/menuhook/setup.py b/src/packages/menuhook/setup.py similarity index 100% rename from menuhook/setup.py rename to src/packages/menuhook/setup.py diff --git a/mxvscode/.gitignore b/src/packages/mxvscode/.gitignore similarity index 100% rename from mxvscode/.gitignore rename to src/packages/mxvscode/.gitignore diff --git a/mxvscode/LICENSE b/src/packages/mxvscode/LICENSE similarity index 100% rename from mxvscode/LICENSE rename to src/packages/mxvscode/LICENSE diff --git a/mxvscode/README.md b/src/packages/mxvscode/README.md similarity index 100% rename from mxvscode/README.md rename to src/packages/mxvscode/README.md diff --git a/mxvscode/mxvscode/__init__.py b/src/packages/mxvscode/mxvscode/__init__.py similarity index 100% rename from mxvscode/mxvscode/__init__.py rename to src/packages/mxvscode/mxvscode/__init__.py diff --git a/mxvscode/setup.py b/src/packages/mxvscode/setup.py similarity index 100% rename from mxvscode/setup.py rename to src/packages/mxvscode/setup.py diff --git a/quickpreview/LICENSE b/src/packages/quickpreview/LICENSE similarity index 100% rename from quickpreview/LICENSE rename to src/packages/quickpreview/LICENSE diff --git a/quickpreview/README.md b/src/packages/quickpreview/README.md similarity index 88% rename from quickpreview/README.md rename to src/packages/quickpreview/README.md index 06bbef5..1158138 100644 --- a/quickpreview/README.md +++ b/src/packages/quickpreview/README.md @@ -2,7 +2,7 @@ ![Preview](doc/Preview.png) -[Original MaxScript Tutorial](https://help.autodesk.com/view/3DSMAX/2020/ENU/?guid=GUID-333382D0-57AF-4797-98F2-C2BE09442607) +[Original MaxScript Tutorial](https://help.autodesk.com/view/MAXDEV/2021/ENU/?guid=GUID-333382D0-57AF-4797-98F2-C2BE09442607) [Source Code](quickpreview/__init__.py) *Goals:* @@ -39,7 +39,7 @@ entry point for 3ds Max). ## Understanding the code We use `rt.getDir(rt.Name("preview"))` to find the default directory -for previews ([3ds Max System Directories](https://help.autodesk.com/view/3DSMAX/2020/ENU/?guid=GUID-F7577416-051E-478C-BB5D-81243BAAC8EC#GUID-F7577416-051E-478C-BB5D-81243BAAC8EC)). +for previews ([3ds Max System Directories](https://help.autodesk.com/view/MAXDEV/2021/ENU/?guid=GUID-F7577416-051E-478C-BB5D-81243BAAC8EC#GUID-F7577416-051E-478C-BB5D-81243BAAC8EC)). We concatenate "quickpreview.avi" using the standard Python `path.join` function. ```python @@ -79,7 +79,7 @@ We then force a garbage collection: rt.gc() ``` -And launch the [RAMPlayer](https://help.autodesk.com/view/3DSMAX/2020/ENU/?guid=GUID-650BE5AA-1DFB-4847-99B2-777A281490F6#GUID-650BE5AA-1DFB-4847-99B2-777A281490F6) for our generated avi: +And launch the [RAMPlayer](https://help.autodesk.com/view/MAXDEV/2021/ENU/?guid=GUID-650BE5AA-1DFB-4847-99B2-777A281490F6#GUID-650BE5AA-1DFB-4847-99B2-777A281490F6) for our generated avi: ```python rt.ramplayer(preview_name, "") diff --git a/quickpreview/doc/Preview.png b/src/packages/quickpreview/doc/Preview.png similarity index 100% rename from quickpreview/doc/Preview.png rename to src/packages/quickpreview/doc/Preview.png diff --git a/quickpreview/quickpreview/__init__.py b/src/packages/quickpreview/quickpreview/__init__.py similarity index 100% rename from quickpreview/quickpreview/__init__.py rename to src/packages/quickpreview/quickpreview/__init__.py diff --git a/quickpreview/setup.py b/src/packages/quickpreview/setup.py similarity index 100% rename from quickpreview/setup.py rename to src/packages/quickpreview/setup.py diff --git a/reloadmod/Capture.png b/src/packages/reloadmod/Capture.png similarity index 100% rename from reloadmod/Capture.png rename to src/packages/reloadmod/Capture.png diff --git a/reloadmod/LICENSE b/src/packages/reloadmod/LICENSE similarity index 100% rename from reloadmod/LICENSE rename to src/packages/reloadmod/LICENSE diff --git a/reloadmod/README.md b/src/packages/reloadmod/README.md similarity index 100% rename from reloadmod/README.md rename to src/packages/reloadmod/README.md diff --git a/reloadmod/reloadmod/__init__.py b/src/packages/reloadmod/reloadmod/__init__.py similarity index 100% rename from reloadmod/reloadmod/__init__.py rename to src/packages/reloadmod/reloadmod/__init__.py diff --git a/reloadmod/reloadmod/reload.py b/src/packages/reloadmod/reloadmod/reload.py similarity index 100% rename from reloadmod/reloadmod/reload.py rename to src/packages/reloadmod/reloadmod/reload.py diff --git a/reloadmod/setup.py b/src/packages/reloadmod/setup.py similarity index 100% rename from reloadmod/setup.py rename to src/packages/reloadmod/setup.py diff --git a/removeallmaterials/LICENSE b/src/packages/removeallmaterials/LICENSE similarity index 100% rename from removeallmaterials/LICENSE rename to src/packages/removeallmaterials/LICENSE diff --git a/removeallmaterials/README.md b/src/packages/removeallmaterials/README.md similarity index 93% rename from removeallmaterials/README.md rename to src/packages/removeallmaterials/README.md index 21a797b..9b98475 100644 --- a/removeallmaterials/README.md +++ b/src/packages/removeallmaterials/README.md @@ -1,5 +1,5 @@ # HowTo: removeallmaterials -[Original MaxScript Tutorial](https://help.autodesk.com/view/3DSMAX/2020/ENU/?guid=GUID-BB996DFB-0367-4DFF-A1CC-50BEB3A97757) +[Original MaxScript Tutorial](https://help.autodesk.com/view/MAXDEV/2021/ENU/?guid=GUID-BB996DFB-0367-4DFF-A1CC-50BEB3A97757) [Source Code](removeallmaterials/__init__.py) *Goals:* diff --git a/removeallmaterials/removeallmaterials/__init__.py b/src/packages/removeallmaterials/removeallmaterials/__init__.py similarity index 100% rename from removeallmaterials/removeallmaterials/__init__.py rename to src/packages/removeallmaterials/removeallmaterials/__init__.py diff --git a/removeallmaterials/setup.py b/src/packages/removeallmaterials/setup.py similarity index 100% rename from removeallmaterials/setup.py rename to src/packages/removeallmaterials/setup.py diff --git a/renameselected/LICENSE b/src/packages/renameselected/LICENSE similarity index 100% rename from renameselected/LICENSE rename to src/packages/renameselected/LICENSE diff --git a/renameselected/README.md b/src/packages/renameselected/README.md similarity index 95% rename from renameselected/README.md rename to src/packages/renameselected/README.md index bb5c696..fe72b14 100644 --- a/renameselected/README.md +++ b/src/packages/renameselected/README.md @@ -2,7 +2,7 @@ ![Dialog](doc/Dialog.png) -[Original MaxScript Tutorial](https://help.autodesk.com/view/3DSMAX/2020/ENU/?guid=GUID-5986CAD3-BB68-47BC-B4B2-EF84C4659271) +[Original MaxScript Tutorial](https://help.autodesk.com/view/MAXDEV/2021/ENU/?guid=GUID-5986CAD3-BB68-47BC-B4B2-EF84C4659271) [Source Code](renameselected/__init__.py) *Goals:* diff --git a/renameselected/doc/Dialog.png b/src/packages/renameselected/doc/Dialog.png similarity index 100% rename from renameselected/doc/Dialog.png rename to src/packages/renameselected/doc/Dialog.png diff --git a/renameselected/renameselected/__init__.py b/src/packages/renameselected/renameselected/__init__.py similarity index 100% rename from renameselected/renameselected/__init__.py rename to src/packages/renameselected/renameselected/__init__.py diff --git a/renameselected/renameselected/ui.py b/src/packages/renameselected/renameselected/ui.py similarity index 100% rename from renameselected/renameselected/ui.py rename to src/packages/renameselected/renameselected/ui.py diff --git a/renameselected/setup.py b/src/packages/renameselected/setup.py similarity index 100% rename from renameselected/setup.py rename to src/packages/renameselected/setup.py diff --git a/singleinstancedlg/LICENSE b/src/packages/singleinstancedlg/LICENSE similarity index 100% rename from singleinstancedlg/LICENSE rename to src/packages/singleinstancedlg/LICENSE diff --git a/singleinstancedlg/README.md b/src/packages/singleinstancedlg/README.md similarity index 100% rename from singleinstancedlg/README.md rename to src/packages/singleinstancedlg/README.md diff --git a/singleinstancedlg/setup.py b/src/packages/singleinstancedlg/setup.py similarity index 100% rename from singleinstancedlg/setup.py rename to src/packages/singleinstancedlg/setup.py diff --git a/singleinstancedlg/singleinstancedlg/__init__.py b/src/packages/singleinstancedlg/singleinstancedlg/__init__.py similarity index 100% rename from singleinstancedlg/singleinstancedlg/__init__.py rename to src/packages/singleinstancedlg/singleinstancedlg/__init__.py diff --git a/singleinstancedlg/singleinstancedlg/ui.py b/src/packages/singleinstancedlg/singleinstancedlg/ui.py similarity index 100% rename from singleinstancedlg/singleinstancedlg/ui.py rename to src/packages/singleinstancedlg/singleinstancedlg/ui.py diff --git a/speedsheet/LICENSE b/src/packages/speedsheet/LICENSE similarity index 100% rename from speedsheet/LICENSE rename to src/packages/speedsheet/LICENSE diff --git a/speedsheet/README.md b/src/packages/speedsheet/README.md similarity index 92% rename from speedsheet/README.md rename to src/packages/speedsheet/README.md index 2dc2eac..c6b94eb 100644 --- a/speedsheet/README.md +++ b/src/packages/speedsheet/README.md @@ -2,7 +2,7 @@ ![Speedsheet](doc/Speedsheet.png) -[Original MaxScript Tutorial](https://help.autodesk.com/view/3DSMAX/2020/ENU/?guid=GUID-2DB3A775-776F-4D63-BDFB-D99523ECB69D) +[Original MaxScript Tutorial](https://help.autodesk.com/view/MAXDEV/2021/ENU/?guid=GUID-2DB3A775-776F-4D63-BDFB-D99523ECB69D) [Source Code](speedsheet/__init__.py) *Goals:* @@ -13,7 +13,7 @@ Non Goal: - explaining how to connect a Python function to a menu item (this is done -in other samples like [removeallmaterials](/removeallmaterials/README.md)) +in other samples like [removeallmaterials](/src/packages/removeallmaterials/README.md)) ## Explanations @@ -62,7 +62,7 @@ simplifies the code and makes it more robust at the same time. Next, the code produces a list of objects at the first frame of the animation. Everything inside the `with pymxs.attime(sometime)` block will -happen at `sometime` in the time line (this is the same as the [at time](https://help.autodesk.com/view/3DSMAX/2020/ENU/?guid=GUID-4E9CCD61-F575-42E1-8654-315DDF6C6A26#GUID-4E9CCD61-F575-42E1-8654-315DDF6C6A26) +happen at `sometime` in the time line (this is the same as the [at time](https://help.autodesk.com/view/MAXDEV/2021/ENU/?guid=GUID-4E9CCD61-F575-42E1-8654-315DDF6C6A26#GUID-4E9CCD61-F575-42E1-8654-315DDF6C6A26) construction in MAXScript): ```python diff --git a/speedsheet/doc/Speedsheet.png b/src/packages/speedsheet/doc/Speedsheet.png similarity index 100% rename from speedsheet/doc/Speedsheet.png rename to src/packages/speedsheet/doc/Speedsheet.png diff --git a/speedsheet/setup.py b/src/packages/speedsheet/setup.py similarity index 100% rename from speedsheet/setup.py rename to src/packages/speedsheet/setup.py diff --git a/speedsheet/speedsheet/__init__.py b/src/packages/speedsheet/speedsheet/__init__.py similarity index 100% rename from speedsheet/speedsheet/__init__.py rename to src/packages/speedsheet/speedsheet/__init__.py diff --git a/threadprogressbar/LICENSE b/src/packages/threadprogressbar/LICENSE similarity index 100% rename from threadprogressbar/LICENSE rename to src/packages/threadprogressbar/LICENSE diff --git a/threadprogressbar/README.md b/src/packages/threadprogressbar/README.md similarity index 97% rename from threadprogressbar/README.md rename to src/packages/threadprogressbar/README.md index 4ce888a..a62641a 100644 --- a/threadprogressbar/README.md +++ b/src/packages/threadprogressbar/README.md @@ -7,7 +7,7 @@ *Non Goal:* - explaining how to connect a Python function to a menu item (this is done -in other samples like [removeallmaterials](/removeallmaterials/README.md)) +in other samples like [removeallmaterials](/src/packages/removeallmaterials/README.md)) ## Explanations diff --git a/threadprogressbar/doc/Progress.png b/src/packages/threadprogressbar/doc/Progress.png similarity index 100% rename from threadprogressbar/doc/Progress.png rename to src/packages/threadprogressbar/doc/Progress.png diff --git a/threadprogressbar/setup.py b/src/packages/threadprogressbar/setup.py similarity index 100% rename from threadprogressbar/setup.py rename to src/packages/threadprogressbar/setup.py diff --git a/threadprogressbar/threadprogressbar/__init__.py b/src/packages/threadprogressbar/threadprogressbar/__init__.py similarity index 100% rename from threadprogressbar/threadprogressbar/__init__.py rename to src/packages/threadprogressbar/threadprogressbar/__init__.py diff --git a/threadprogressbar/threadprogressbar/ui.py b/src/packages/threadprogressbar/threadprogressbar/ui.py similarity index 100% rename from threadprogressbar/threadprogressbar/ui.py rename to src/packages/threadprogressbar/threadprogressbar/ui.py diff --git a/transformlock/LICENSE b/src/packages/transformlock/LICENSE similarity index 100% rename from transformlock/LICENSE rename to src/packages/transformlock/LICENSE diff --git a/transformlock/README.md b/src/packages/transformlock/README.md similarity index 91% rename from transformlock/README.md rename to src/packages/transformlock/README.md index 62f366c..8f58cac 100644 --- a/transformlock/README.md +++ b/src/packages/transformlock/README.md @@ -1,6 +1,6 @@ # HowTo: transformlock -[Original MaxScript Tutorial](https://help.autodesk.com/view/3DSMAX/2020/ENU/?guid=GUID-8EB13535-72B4-439C-94D3-E93434BA163B) +[Original MaxScript Tutorial](https://help.autodesk.com/view/MAXDEV/2021/ENU/?guid=GUID-8EB13535-72B4-439C-94D3-E93434BA163B) [Source Code](transformlock/__init__.py) *Goals:* @@ -42,7 +42,7 @@ from pymxs import runtime as rt ``` The core business logic of the program comes from the lock\_selection function. This uses -the setTransformLockFlags of ([node common methods](https://help.autodesk.com/view/3DSMAX/2020/ENU/?guid=GUID-D1D7EB56-A370-4B07-99B4-BC779FB87CAF#GUID-D1D7EB56-A370-4B07-99B4-BC779FB87CAF__SECTION_130281B392F64446B4AE8562EAD75531)) +the setTransformLockFlags of ([node common methods](https://help.autodesk.com/view/MAXDEV/2021/ENU/?guid=GUID-D1D7EB56-A370-4B07-99B4-BC779FB87CAF#GUID-D1D7EB56-A370-4B07-99B4-BC779FB87CAF__SECTION_130281B392F64446B4AE8562EAD75531)) to lock all (`rt.Name("all")` transforms on the whole selection (`rt.selection`)): ```python diff --git a/transformlock/setup.py b/src/packages/transformlock/setup.py similarity index 100% rename from transformlock/setup.py rename to src/packages/transformlock/setup.py diff --git a/transformlock/transformlock/__init__.py b/src/packages/transformlock/transformlock/__init__.py similarity index 100% rename from transformlock/transformlock/__init__.py rename to src/packages/transformlock/transformlock/__init__.py diff --git a/zdepthchannel/LICENSE b/src/packages/zdepthchannel/LICENSE similarity index 100% rename from zdepthchannel/LICENSE rename to src/packages/zdepthchannel/LICENSE diff --git a/zdepthchannel/README.md b/src/packages/zdepthchannel/README.md similarity index 97% rename from zdepthchannel/README.md rename to src/packages/zdepthchannel/README.md index ba7db10..a9bb13e 100644 --- a/zdepthchannel/README.md +++ b/src/packages/zdepthchannel/README.md @@ -2,7 +2,7 @@ ![ZDpeth](doc/ZDepth.png) -[Original MaxScript Tutorial](https://help.autodesk.com/view/3DSMAX/2020/ENU/?guid=GUID-3667A33C-E3E4-4F39-A480-3713240838F1) +[Original MaxScript Tutorial](https://help.autodesk.com/view/MAXDEV/2021/ENU/?guid=GUID-3667A33C-E3E4-4F39-A480-3713240838F1) [Source Code](zdepthchannel/__init__.py) The 3ds Max default scanline renderer generates a multitude of additional data diff --git a/zdepthchannel/doc/ZDepth.png b/src/packages/zdepthchannel/doc/ZDepth.png similarity index 100% rename from zdepthchannel/doc/ZDepth.png rename to src/packages/zdepthchannel/doc/ZDepth.png diff --git a/zdepthchannel/setup.py b/src/packages/zdepthchannel/setup.py similarity index 100% rename from zdepthchannel/setup.py rename to src/packages/zdepthchannel/setup.py diff --git a/zdepthchannel/zdepthchannel/__init__.py b/src/packages/zdepthchannel/zdepthchannel/__init__.py similarity index 100% rename from zdepthchannel/zdepthchannel/__init__.py rename to src/packages/zdepthchannel/zdepthchannel/__init__.py diff --git a/pystartup/README.md b/src/pystartup/README.md similarity index 95% rename from pystartup/README.md rename to src/pystartup/README.md index fae6e4b..8829316 100644 --- a/pystartup/README.md +++ b/src/pystartup/README.md @@ -25,8 +25,8 @@ yourpackagename works) and startup is an exported function of yourpackagename that will be called during startup. Most if not all Python samples in this repo implement this entry -point. [transformlock's setup script](/transformlock/setup.py) can -be taken as en example as well as [transformlock's \_\_init\_\_.py](/transformlock/transformlock/__init__.py). +point. [transformlock's setup script](/src/packages/transformlock/setup.py) can +be taken as en example as well as [transformlock's \_\_init\_\_.py](/src/packages/transformlock/transformlock/__init__.py). ## The maxscript code diff --git a/pystartup/pystartup.ms b/src/pystartup/pystartup.ms similarity index 100% rename from pystartup/pystartup.ms rename to src/pystartup/pystartup.ms diff --git a/src/samples/PySide2/combine_meshes.py b/src/samples/PySide2/combine_meshes.py new file mode 100644 index 0000000..20c3aae --- /dev/null +++ b/src/samples/PySide2/combine_meshes.py @@ -0,0 +1,53 @@ +''' +Demonstrates combining the mesh of two scene nodes +''' +from PySide2.QtWidgets import QVBoxLayout, QPushButton, QLabel, QDialog, QMessageBox +from pymxs import runtime as rt # pylint: disable=import-error +from qtmax import GetQMaxMainWindow + +def combine_two_meshes(): + """ + Convert the two provided objects to meshes and merges them as a single editable mesh. + """ + if len(rt.getCurrentSelection()) != 2: + msg = "Please select 2 nodes to combine." + show_alert(msg) + else: + first_item_selected = rt.convertToMesh(rt.getCurrentSelection()[0]) + second_item_selected = rt.convertToMesh(rt.getCurrentSelection()[1]) + # create a new, empty editable mesh for the combined meshes + new_obj = rt.Editable_mesh() + # combine 'first_item_selected' and 'second_item_selected' into the new mesh + mesh_operation = rt.meshOp + mesh_operation.attach(new_obj, first_item_selected, deleteSourceNode=False) + mesh_operation.attach(new_obj, second_item_selected, deleteSourceNode=False) + +def show_alert(message): + """ + Display a message using a Qt Message Box. + """ + msg_box = QMessageBox() + msg_box.setText(message) + msg_box.exec_() + +def demo_combine_meshes(): + """ + Demonstrates combining the mesh of two scene nodes + Prompt user to select two nodes to be merged and merge them. + """ + dialog = QDialog(GetQMaxMainWindow()) + dialog.resize(250, 100) + dialog.setWindowTitle('DEMO - Combine 2 Nodes') + + main_layout = QVBoxLayout() + label = QLabel("Combine 2 Nodes") + main_layout.addWidget(label) + + combine_btn = QPushButton("Combine") + combine_btn.clicked.connect(combine_two_meshes) + main_layout.addWidget(combine_btn) + + dialog.setLayout(main_layout) + dialog.show() + +demo_combine_meshes() diff --git a/src/samples/PySide2/cylinder_icon_48.png b/src/samples/PySide2/cylinder_icon_48.png new file mode 100644 index 0000000..c3a4b5c Binary files /dev/null and b/src/samples/PySide2/cylinder_icon_48.png differ diff --git a/src/samples/PySide2/docking_widgets.py b/src/samples/PySide2/docking_widgets.py new file mode 100644 index 0000000..f4102e1 --- /dev/null +++ b/src/samples/PySide2/docking_widgets.py @@ -0,0 +1,94 @@ +''' + Demonstrates how to create a QWidget with PySide2 and attach it to the 3dsmax main window. + Creates two types of dockable widgets, a QDockWidget and a QToolbar +''' + +import os +import ctypes + +from PySide2 import QtCore +from PySide2 import QtGui +from PySide2.QtWidgets import QMainWindow, QDockWidget, QToolButton, QToolBar, QAction + +from pymxs import runtime as rt +from qtmax import GetQMaxMainWindow + +def get_pos_to_dock_toolbar(dock_widget): + """ + Get the docking widget position based on its size + """ + space_between_widgets = 20 # Arbritrary hard coded value + dock_widget_rect = dock_widget.geometry() + x_pos = dock_widget_rect.x() + y_pos = dock_widget_rect.bottom() + space_between_widgets + return QtCore.QPoint(x_pos, y_pos) + +def make_toolbar_floating(toolbar, pos): + """ + Set the toolbar widget properties to act as a tool floating window + """ + toolbar.setWindowFlags( + QtCore.Qt.Tool | QtCore.Qt.FramelessWindowHint | QtCore.Qt.X11BypassWindowManagerHint) + toolbar.move(pos) + toolbar.adjustSize() + toolbar.show() + QtCore.QMetaObject.invokeMethod(toolbar, \ + "topLevelChanged", \ + QtCore.Qt.DirectConnection, \ + QtCore.QGenericArgument("bool", ctypes.c_void_p(True))) + +def create_cylinder(): + """ + Create a cylinder node with predetermined radius and height values. + """ + rt.Cylinder(radius=10, height=30) + rt.redrawViews() + +def demo_docking_widgets(): + """ + Demonstrates how to create a QWidget with PySide2 and attach it to the 3dsmax main window. + Creates two types of dockable widgets, a QDockWidget and a QToolbar + """ + # Retrieve 3ds Max Main Window QWdiget + main_window = GetQMaxMainWindow() + + # QAction reused by both dockable widgets. + cylinder_icon_path = os.path.dirname(os.path.realpath(__file__)) + "\\cylinder_icon_48.png" + cylinder_icon = QtGui.QIcon(cylinder_icon_path) + create_cyl_action = QAction(cylinder_icon, u"Create Cylinder", main_window) + create_cyl_action.triggered.connect(create_cylinder) + + # QDockWidget construction and placement over the main window + dock_widget = QDockWidget(main_window) + + # Set for position persistence + dock_widget.setObjectName("Creators") + # Set to see dock widget name in toolbar customize popup + dock_widget.setWindowTitle("Creators") + dock_tool_button = QToolButton() + dock_tool_button.setAutoRaise(True) + dock_tool_button.setDefaultAction(create_cyl_action) + dock_tool_button.setToolButtonStyle(QtCore.Qt.ToolButtonTextOnly) + dock_widget.setWidget(dock_tool_button) + + main_window.addDockWidget(QtCore.Qt.LeftDockWidgetArea, dock_widget) + dock_widget.setFloating(True) + dock_widget.show() + + # QToolBar construction and attachement to main window + toolbar_widget = QToolBar(main_window) + + # Set for position persistence + toolbar_widget.setObjectName("Creators TB") + # Set to see dock widget name in toolbar customize popup + toolbar_widget.setWindowTitle("Creators TB") + toolbar_widget.setFloatable(True) + toolbar_widget.addAction(create_cyl_action) + + main_window.addToolBar(QtCore.Qt.BottomToolBarArea, toolbar_widget) + toolbar_widget.show() + + toolbar_position = get_pos_to_dock_toolbar(dock_widget) + make_toolbar_floating(toolbar_widget, toolbar_position) + +demo_docking_widgets() diff --git a/src/samples/PySide2/simple_dialog.py b/src/samples/PySide2/simple_dialog.py new file mode 100644 index 0000000..3fc5501 --- /dev/null +++ b/src/samples/PySide2/simple_dialog.py @@ -0,0 +1,50 @@ +''' + Demonstrates how to create a QDialog with PySide2 and attach it to the 3ds Max main window. +''' + +from PySide2.QtWidgets import QDialog, QLabel, QVBoxLayout, QPushButton +from pymxs import runtime as rt +from qtmax import GetQMaxMainWindow + +def create_cylinder(): + """ + Create a cylinder node with predetermined radius and height values. + """ + rt.Cylinder(radius=10, height=30) + # force a viewport update for the node to appear + rt.redrawViews() + +class PyMaxDialog(QDialog): + """ + Custom dialog attached to the 3ds Max main window + Message label and action push button to create a cylinder in the 3ds Max scene graph + """ + def __init__(self, parent=None): + super(PyMaxDialog, self).__init__(parent) + self.setWindowTitle('Pyside2 Qt Window') + self.init_ui() + + def init_ui(self): + """ Prepare Qt UI layout for custom dialog """ + main_layout = QVBoxLayout() + label = QLabel("Click button to create a cylinder in the scene") + main_layout.addWidget(label) + + cylinder_btn = QPushButton("Cylinder") + cylinder_btn.clicked.connect(create_cylinder) + main_layout.addWidget(cylinder_btn) + + self.setLayout(main_layout) + self.resize(250, 100) + +def demo_simple_dialog(): + """ + Entry point for QDialog demo making use of PySide2 and pymxs + """ + # reset 3ds Max + rt.resetMaxFile(rt.Name('noPrompt')) + + dialog = PyMaxDialog(GetQMaxMainWindow()) + dialog.show() + +demo_simple_dialog() diff --git a/src/samples/PySide2/test_ui.ui b/src/samples/PySide2/test_ui.ui new file mode 100644 index 0000000..e7286ab --- /dev/null +++ b/src/samples/PySide2/test_ui.ui @@ -0,0 +1,99 @@ + + + TabWidget + + + + 0 + 0 + 374 + 243 + + + + TabWidget + + + 0 + + + + + 0 + 0 + + + + Tab 1 + + + + QLayout::SetNoConstraint + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + + PushButton + + + + + + + ... + + + + + + + + + + + + + + 0 + 0 + + + + Tab 2 + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + + + + + diff --git a/src/samples/PySide2/ui_loader.py b/src/samples/PySide2/ui_loader.py new file mode 100644 index 0000000..3369960 --- /dev/null +++ b/src/samples/PySide2/ui_loader.py @@ -0,0 +1,34 @@ +''' + Demonstrates loading .ui files with PySide2 +''' +import os +from PySide2.QtWidgets import QMainWindow +from PySide2.QtCore import QFile +from PySide2.QtUiTools import QUiLoader +from pymxs import runtime as rt +from qtmax import GetQMaxMainWindow + +class MyWindow(QMainWindow): + """ + Main window class object loading a .ui file + """ + def __init__(self, parent=None): + super(MyWindow, self).__init__(parent) + self.setWindowTitle('Pyside2 Qt Window') + self.init_ui() + + def init_ui(self): + """ Prepare Qt UI layout for main window content """ + ui_file = QFile(os.path.dirname(os.path.realpath(__file__)) + "\\test_ui.ui") + ui_file.open(QFile.ReadOnly) + self.loaded_ui = QUiLoader().load(ui_file, self) + ui_file.close() + +def demo_ui_loader(): + """ + Entry point to demonstrate how to load a .ui file + """ + win = MyWindow(GetQMaxMainWindow()) + win.show() + +demo_ui_loader() diff --git a/src/samples/__init__.py b/src/samples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/samples/pymxs/animation.py b/src/samples/pymxs/animation.py new file mode 100644 index 0000000..3445514 --- /dev/null +++ b/src/samples/pymxs/animation.py @@ -0,0 +1,77 @@ +''' + Demonstrates simple animation. +''' +from pymxs import runtime as rt # pylint: disable=import-error +import pymxs as mx # pylint: disable=import-error + +print("Hello World Animation") + +def print_interval(interval): + '''Prints an animation interval''' + print(f"Current Animation Range: [{interval.start},{interval.end}]") + +def set_animation_ranges(): + '''Changes the animation range from the default of 100 frames to 200 frames''' + frames = 200 + print_interval(rt.animationRange) + rt.animationRange = rt.Interval(0, frames) + # The animation slider now shows 200 frames + print_interval(rt.animationRange) + + +def animate_transform(thing): + '''Records an animation on the provided object''' + # select the object to animate so we will see the keyframes in the timeslider + rt.select(thing) + + # animate + with mx.animate(True): + with mx.redraw(True): + with mx.attime(30): + thing.pos = rt.Point3(50, 0, 0) + + with mx.attime(60): + thing.Pos = rt.Point3(100, 50, 0) + + with mx.attime(90): + thing.Pos = rt.Point3(50, 100, 0) + + with mx.attime(120): + thing.Pos = rt.Point3(0, 100, 0) + + with mx.attime(150): + thing.Pos = rt.Point3(-50, 50, 0) + + with mx.attime(180): + thing.Pos = rt.Point3(0, 0, 0) + +def playback_animation(): + '''Play back the animation 3 times''' + rt.playbackLoop = False + # play animation + print("Playing back Animation first time") + rt.sliderTime = 0 + rt.timeConfiguration.playbackSpeed = 3 # normal speed + rt.playAnimation() + + # replay it + print("Playing back Animation second time") + rt.timeConfiguration.playbackSpeed = 4 # double speed + rt.sliderTime = 0 + rt.playAnimation() + + # replay it, faster + print("Playing back Animation third time, faster") + rt.sliderTime = 0 + rt.timeConfiguration.playbackSpeed = 5 # 4x speed + rt.playAnimation() + +def demo_animation(): + '''Show how to do animation''' + rt.resetMaxFile(rt.Name('noPrompt')) + sphere = rt.sphere() + set_animation_ranges() + animate_transform(sphere) + playback_animation() + +demo_animation() diff --git a/src/samples/pymxs/app_chunk.py b/src/samples/pymxs/app_chunk.py new file mode 100644 index 0000000..89f0f5e --- /dev/null +++ b/src/samples/pymxs/app_chunk.py @@ -0,0 +1,53 @@ +''' + Demonstrates how to manage user specified data for any object derived from Animatable. +''' +from pymxs import runtime as rt # pylint: disable=import-error + +def create_scene(): + """Create and save scene_with_app_chunk.max""" + rt.resetMaxFile(rt.Name('noPrompt')) + + # Create a teapot, a scene node and a material instance, they are all + # objects of Animatable + node = rt.teapot() + teapot = node.baseObject + mtl = rt.StandardMaterial() + node.Material = mtl + node.name = "MyTeapot123" + + # Now add some user specified strings to these objects + rt.setAppdata(teapot, 112, "blah comit") + rt.setAppdata(teapot, 1234, "I'm a teapot!") + rt.setAppdata(teapot, 2345, u"我是一个茶壶!") + + rt.setAppdata(node, 5678, "Node of teapot") + rt.setAppdata(node, 7890, "This is to be removed") + rt.deleteAppdata(node, 7890) + + rt.setAppdata(mtl, 4567, "Material of teapot") + rt.saveMaxFile("scene_with_app_chunk.max") + print("scene with AppChunk is saved.") + + +def load_and_verify(): + """Load and verify scene_with_app_chunk.max""" + rt.resetMaxFile(rt.Name('noPrompt')) + rt.loadMaxFile("scene_with_app_chunk.max") + print("scene with AppChunk is loaded.") + # Find the "MyTeapot123" node + teapot_node = rt.getNodeByName("MyTeapot123") + + if teapot_node is None: + print("Error: Incorrect saved scene.") + else: + print(rt.getAppData(teapot_node, 678)) + obj = teapot_node.baseObject + print(rt.getAppData(obj, 1234)) + print(rt.getAppData(obj, 2345)) + rt.clearAllAppData(obj) + print("No 9432 app data {}".format(rt.getAppData(obj, 9432) is None)) + print("No 7890 app data {}".format(rt.getAppData(teapot_node, 9432) is None)) + print(rt.getAppData(teapot_node.Material, 4567)) + +create_scene() +load_and_verify() diff --git a/src/samples/pymxs/apply_material.py b/src/samples/pymxs/apply_material.py new file mode 100644 index 0000000..ca01c71 --- /dev/null +++ b/src/samples/pymxs/apply_material.py @@ -0,0 +1,29 @@ +''' + Applies a standard material to all nodes in the scene. + Also shows the use of generator functions in Python. +''' +from pymxs import runtime as rt # pylint: disable=import-error + +def create_sphere(): + """Create a sphere of radius 5.""" + return rt.sphere(radius=5) + +def solid_material(color): + """Create a material.""" + material = rt.StandardMaterial() + material.Ambient = color + material.Diffuse = color + material.Specular = rt.Color(255, 255, 255) + material.Shininess = 50.0 + material.ShinyStrength = 70.0 + material.SpecularLevel = 70.0 + return material + +def apply_material_to_nodes(material, nodes=rt.rootnode.children): + """Apply a material to multiple nodes.""" + for node in nodes: + node.Material = material + +create_sphere() +MAT = solid_material(rt.Color(0, 0, 255)) +apply_material_to_nodes(MAT) diff --git a/src/samples/pymxs/assets.py b/src/samples/pymxs/assets.py new file mode 100644 index 0000000..7269251 --- /dev/null +++ b/src/samples/pymxs/assets.py @@ -0,0 +1,10 @@ +''' + Lists all of the assets in a file. +''' +from pymxs import runtime as rt # pylint: disable=import-error + +NASSETS = rt.AssetManager.GetNumAssets() +print(f"There are {NASSETS} assets created") +for i in range(NASSETS): + a = rt.AssetManager.GetAssetByIndex(i + 1) + print(f"Asset id = {a.GetAssetId()}, type = {a.getType()}, file = {a.getfilename()}") diff --git a/src/samples/pymxs/bent_cylinder.py b/src/samples/pymxs/bent_cylinder.py new file mode 100644 index 0000000..1d7a38a --- /dev/null +++ b/src/samples/pymxs/bent_cylinder.py @@ -0,0 +1,15 @@ +''' + Demonstrates creating a cylinder and appling a bend modifier. +''' +from pymxs import runtime as rt # pylint: disable=import-error + +def main(): + """Create a cylinder and add a bend modifier to it.""" + cyl = rt.cylinder() + cyl.radius = 10 + cyl.height = 30 + bend = rt.Bend() + bend.bendAngle = 45 + rt.addModifier(cyl, bend) + +main() diff --git a/src/samples/pymxs/class_types.py b/src/samples/pymxs/class_types.py new file mode 100644 index 0000000..7b94cd2 --- /dev/null +++ b/src/samples/pymxs/class_types.py @@ -0,0 +1,125 @@ +''' + Demonstrates creating many different types of scene objects that are visible in the viewport. + The scene objects are grouped by type. + The types created are Cameras, Lights, Geometric Objects, Shapes, Helpers, Modifiers + and Materials. +''' +from pymxs import runtime as rt # pylint: disable=import-error +OBJECT_DIMENSION = 5.0 +Y_STEP = OBJECT_DIMENSION * 4 +X_STEP = OBJECT_DIMENSION * 2.0 + +def create_box(): + """Create a box.""" + box = rt.box() + box.Height = OBJECT_DIMENSION + box.Width = OBJECT_DIMENSION + box.Length = OBJECT_DIMENSION + return box + +def create_text(pos, message): + """Create a text.""" + tex = rt.text() + tex.size = Y_STEP + tex.text = message + tex.position = rt.Point3(pos.x, pos.y - OBJECT_DIMENSION, pos.z) + tex.wirecolor = rt.Color(255, 128, 255) + +def create_teapot(): + """Create a teapot.""" + teapot = rt.teapot() + teapot.radius = OBJECT_DIMENSION + return teapot + +def layout_objects(title, cases, y_position, x_offset_text=-45): + """Layout a list of nodes in a line""" + create_text(rt.Point3(x_offset_text, y_position, 0), title) + x_position = 0.0 + for gen in cases: + gen.Position = rt.point3(x_position, y_position, 0) + x_position += X_STEP + if (x_position % 260.0) < 0.001: + x_position = 0.0 + y_position += Y_STEP + return y_position + +def create_classes(classes): + """Create all createble instances of the provided classes""" + for obj in classes: + try: + created = obj() + print(created) + yield created + except RuntimeError: + pass + +def create_cameras(y_position): + """Create all creatable cameras""" + print("-- Cameras") + return layout_objects("Cameras", create_classes(rt.camera.classes), y_position) + +def create_lights(y_position): + """Create all creatable lights""" + print("-- Lights") + return layout_objects("Lights", create_classes(rt.light.classes), y_position) + +def create_objects(y_position): + """Create all creatable objects""" + print("-- Geometric Objects") + return layout_objects( + "Geometric Objects", + create_classes(rt.GeometryClass.classes), + y_position, -88.0) + +def create_shapes(y_position): + """Create all creatable shapes""" + print("-- Shapes") + return layout_objects("Shapes", create_classes(rt.shape.classes), y_position) + +def create_helpers(y_position): + """Create all creatable helpers""" + print("-- Helpers") + return layout_objects("Helpers", create_classes(rt.helper.classes), y_position) + +def create_modifiers(y_position): + """Create all creatable modifiers""" + def create(): + for mod in rt.modifier.classes: + try: + created = mod() + print(created) + box = create_box() + rt.addModifier(box, created) + yield box + except RuntimeError: + pass + print("-- Modifiers") + return layout_objects("Modifiers", create(), y_position) + +def create_materials(y_position): + """Create all creatable materials""" + def create(): + for mat in rt.material.classes: + try: + created = mat() + print(mat) + teapot = create_teapot() + teapot.Material = created + yield teapot + except RuntimeError: + pass + print("-- Materials") + return layout_objects("Materials", create(), y_position) + +def create_items(): + """Create all the items in the sample.""" + rt.resetMaxFile(rt.Name('noPrompt')) + y_line = create_materials(0.0) + 40.0 + y_line = create_modifiers(y_line) + 40.0 + y_line = create_helpers(y_line) + 40.0 + y_line = create_shapes(y_line) + 40.0 + y_line = create_objects(y_line) + 40.0 + y_line = create_lights(y_line) + 40.0 + y_line = create_cameras(y_line) + 40.0 + +create_items() diff --git a/src/samples/pymxs/combine_selected_meshes.py b/src/samples/pymxs/combine_selected_meshes.py new file mode 100644 index 0000000..7102c95 --- /dev/null +++ b/src/samples/pymxs/combine_selected_meshes.py @@ -0,0 +1,29 @@ +''' +Demonstrates combining multipe scene nodes to an editable mesh. +''' +from pymxs import runtime as rt # pylint: disable=import-error + +def combine_objects(*args): + """ + Convert the two provided objects to meshes and merges them as a single mesh. + """ + first = rt.convertToMesh(args[0]) + for obj in args[1:]: + rt.attach(first, rt.convertToMesh(obj)) + + return first + +def combine_selected_objects(): + """ + Convert the selected objects into an editable mesh. The selections needs to contain + at least 2 objects to combine. + """ + if len(rt.selection) < 2: + msg = "Please select at least 2 nodes to combine." + print(msg) + rt.messageBox(msg) + else: + # combine all the selected nodes into one editable mesh + combine_objects(*rt.selection) + +combine_selected_objects() diff --git a/src/samples/pymxs/enumerate_parameters.py b/src/samples/pymxs/enumerate_parameters.py new file mode 100644 index 0000000..1da1510 --- /dev/null +++ b/src/samples/pymxs/enumerate_parameters.py @@ -0,0 +1,16 @@ +''' + Creates all geometric objects and lists their parameters. +''' +from pymxs import runtime as rt # pylint: disable=import-error + +for cl in rt.GeometryClass.classes: + try: + obj = cl() + print(f"Properties for class {cl}") + # This would also work: + # rt.showProperties(o) + for pn in rt.getPropNames(obj): + print(f" {pn} {rt.getProperty(obj, pn)}") + except RuntimeError: + # Some geometry classes cannot be instantiated + pass diff --git a/src/samples/pymxs/file_save.py b/src/samples/pymxs/file_save.py new file mode 100644 index 0000000..82883d5 --- /dev/null +++ b/src/samples/pymxs/file_save.py @@ -0,0 +1,9 @@ +""" + Demonstrate saving a 3ds Max file. +""" +from os import path +from pymxs import runtime as rt # pylint: disable=import-error + +FILEPATH = path.join(rt.sysInfo.tempdir, "test.max") +rt.saveMaxFile(FILEPATH) +print(f"Requested filename {FILEPATH}\n path {rt.maxFilePath}\n file {rt.maxFileName}") diff --git a/src/samples/pymxs/get_rendered_normals.py b/src/samples/pymxs/get_rendered_normals.py new file mode 100644 index 0000000..7997985 --- /dev/null +++ b/src/samples/pymxs/get_rendered_normals.py @@ -0,0 +1,17 @@ +''' +An example of how to get the vertices and normals of a node +''' +from pymxs import runtime as rt # pylint: disable=import-error + +def main(): + """Create a box and display its vertices and normals.""" + box = rt.box() + print(f"node name: {box.name}") + trimesh = rt.convertTo(box, rt.TrimeshGeometry) + print(f" verts: {trimesh.numVerts}") + for vert in range(trimesh.numVerts): + normal = rt.getNormal(trimesh, vert + 1) + vertex = rt.getVert(trimesh, vert + 1) + print(f"vertex: {vertex}") + print(f"RNormal: {normal}") +main() diff --git a/src/samples/pymxs/hit_test.py b/src/samples/pymxs/hit_test.py new file mode 100644 index 0000000..8d9a1d6 --- /dev/null +++ b/src/samples/pymxs/hit_test.py @@ -0,0 +1,16 @@ +''' + Performs a hit test on an object in the active viewport. +''' +from pymxs import runtime as rt # pylint: disable=import-error + +def main(): + """Demonstrate hit testing of rays with scene objects.""" + obj = rt.sphere(radius=50) + point = rt.Point2(400, 200) + hit_ray = rt.intersectRay(obj, rt.mapScreenToWorldRay(point)) + print(f"hit success {bool(hit_ray)} for point {point}") + point = rt.Point2(0, 0) + hit_ray = rt.intersectRay(obj, rt.mapScreenToWorldRay(point)) + print(f"hit success {bool(hit_ray)} for point {point}") + +main() diff --git a/src/samples/pymxs/list_scripts.py b/src/samples/pymxs/list_scripts.py new file mode 100644 index 0000000..8e4b736 --- /dev/null +++ b/src/samples/pymxs/list_scripts.py @@ -0,0 +1,10 @@ +''' + Lists all the files in a folder +''' +import os +from pymxs import runtime as rt # pylint: disable=import-error + +PY_SCRIPTS_DIR = os.path.join(rt.getDir(rt.Name("scripts")), 'python') +for root, dirs, files in os.walk(PY_SCRIPTS_DIR, topdown=False): + for name in files: + print(name) diff --git a/src/samples/pymxs/make_instances.py b/src/samples/pymxs/make_instances.py new file mode 100644 index 0000000..b08895d --- /dev/null +++ b/src/samples/pymxs/make_instances.py @@ -0,0 +1,36 @@ +''' + Demonstrates creating instances of a node hierarchy. +''' +import pymxs # pylint: disable=import-error +from pymxs import runtime as rt # pylint: disable=import-error + +INST = rt.Name("instance") + +def create_instance_clones(obj, count, offset): + """Create count clones of obj setting the parent of each clone to the + previous clone.""" + for _ in range(count): + # the maxscript CloneNodes method accepts a named argument called 'newNodes' + # the argument must be sent by reference as it serves as an output argument + # since the argument is not also an input argument, we can simply initialize + # the byref() object as 'None' + # the output argument along with the call result is then returned in a tuple + # note: 'newNodes' returns an array of cloned nodes + # in the current case, only one element is cloned + result, cloned = rt.MaxOps.CloneNodes(obj, cloneType=INST, offset=offset, newNodes=pymxs.byref(None)) + cloned[0].parent = obj + obj = cloned[0] + +def main(): + """Demonstrate cloning""" + rt.resetMaxFile(rt.Name('noPrompt')) + + obj = rt.sphere(radius=3) + create_instance_clones(obj, 10, rt.Point3(5, 0, 0)) + rt.MaxOps.CloneNodes( + obj, + cloneType=INST, + offset=rt.Point3(0, 25, 0), + expandHierarchy=True) + +main() diff --git a/src/samples/pymxs/materials.py b/src/samples/pymxs/materials.py new file mode 100644 index 0000000..f816cdc --- /dev/null +++ b/src/samples/pymxs/materials.py @@ -0,0 +1,76 @@ +''' + Demonstrates how to iterate through materials and and apply them to objects. + It shows how to open the material editor and put materials in the editor. +''' +from pymxs import runtime as rt # pylint: disable=import-error + +def create_floor(): + """Create a rectangle for the floor""" + plane = rt.Plane() + plane.width = 120 + plane.length = 120 + +def print_material_properties(material_instance): + """Print the properties of a given material""" + print("[%s]" % material_instance.name) + for name in rt.getPropNames(material_instance): + print("\t" + name + " = " + str(rt.getProperty(material_instance, name))) + +def create_text(xpos, ypos, rot, message): + """Create a visible label on the ground for a given teapot""" + tex = rt.text() + tex.size = 10 + tex.text = message + tex.position = rt.Point3(xpos, ypos, 0) + tex.rotation = rot + tex.wireColor = rt.Color(255, 128, 255) + +def showcase_materials(materials): + """Create a teapot sample and a visible label for each provided material""" + num_materials = len(materials) + diff = 360.0 / num_materials + teapot_radius = 5.0 + radius = 50.0 + text_radius = 90.0 + index = 0 + i = 0 + + for mat in materials: + position = rt.Point3(radius, 0, 0) + rot = rt.angleAxis(i, rt.Point3(0, 0, 1)) + + teapot = rt.teapot() + teapot.radius = teapot_radius + teapot.position = position + teapot.rotation = rot + teapot.Material = mat + print_material_properties(mat) + + create_text(text_radius, 0, rot, mat.name) + if index < 24: + rt.setMeditMaterial(index + 1, mat) + index += 1 + i += diff + +def sample(): + """Create all existing materials and showcase them.""" + def try_create(mat): + """Try to create a given material. If not creatable return None.""" + try: + return mat() + except RuntimeError: + return None + rt.resetMaxFile(rt.Name('noPrompt')) + # maximize the view (select a view with only the one viewport) + rt.viewport.setLayout(rt.name("layout_1")) + # show the material editor in basic mode + rt.MatEditor.mode = rt.Name("basic") + rt.MatEditor.open() + # create a plane for the floor + create_floor() + # instantiate all materials that can be instantiated + materials = filter(lambda x: x is not None, map(try_create, rt.material.classes)) + # showcase all materials + showcase_materials(list(materials)) + +sample() diff --git a/src/samples/pymxs/mesh_and_cpv.py b/src/samples/pymxs/mesh_and_cpv.py new file mode 100644 index 0000000..3224aaf --- /dev/null +++ b/src/samples/pymxs/mesh_and_cpv.py @@ -0,0 +1,86 @@ +''' + Demonstrates how to create a mesh from scratch and to set color per vertex data. +''' +from pymxs import runtime as rt # pylint: disable=import-error + +def set_edge_visibility(mesh, face, aedge, bedge, cedge): + """Set the visibility of face edges""" + rt.setEdgeVis(mesh, face, 1, aedge) + rt.setEdgeVis(mesh, face, 2, bedge) + rt.setEdgeVis(mesh, face, 3, cedge) + +def make_pyramid_mesh(side=20.0): + """Create a pyramid from vertices and faces.""" + mesh = rt.mesh() + mesh.numverts = 4 + mesh.numfaces = 4 + + halfside = side / 2.0 + rt.SetVert(mesh, 1, rt.Point3(0.0, 0.0, side)) + rt.SetVert(mesh, 2, rt.Point3(-halfside, -halfside, 0.0)) + rt.SetVert(mesh, 3, rt.Point3(-halfside, halfside, 0.0)) + rt.SetVert(mesh, 4, rt.Point3(halfside, 0.0, 0.0)) + + rt.setFace(mesh, 1, 1, 2, 3) + set_edge_visibility(mesh, 1, True, True, False) + + rt.setFace(mesh, 2, 1, 3, 4) + set_edge_visibility(mesh, 2, True, True, False) + + rt.setFace(mesh, 3, 1, 4, 2) + set_edge_visibility(mesh, 2, True, True, False) + + rt.setFace(mesh, 4, 2, 3, 4) + set_edge_visibility(mesh, 2, True, True, False) + + rt.update(mesh) + return mesh + +def output_channel(mesh, channel, name): + """Retrieve and display the information about a given mesh map.""" + print("Channel: " + name) + if not rt.meshop.getMapSupport(mesh, channel): + print(" Not enabled") + return + + vertices = rt.meshop.getNumMapVerts(mesh, channel) + print(f" Number of texture vertices: {vertices}") + for vindex in range(1, vertices + 1): + vertex = rt.meshop.getMapVert(mesh, channel, vindex) + print(f" Texture vertex {vertex.X}, {vertex.Y}, {vertex.Z}") + + faces = rt.meshop.getNumMapFaces(mesh, channel) + print(f" Number of faces: {faces}") + for findex in range(1, faces + 1): + face = rt.meshop.getMapFace(mesh, channel, findex) + print(f" Texture vertex indices {face.X}, {face.Y}, {face.Z}") + print() + +def output_channels(mesh): + """Retrieve and display the information about all channels.""" + nummaps = rt.meshop.getNumMaps(mesh) + print(f"NumMaps: {nummaps}") + print() + output_channel(mesh, 0, "color per vertex") + output_channel(mesh, 1, "texture mapping") + +def main(): + """Create a mesh, color it, and output information about its maps.""" + # reset the scene + rt.resetMaxFile(rt.Name('noPrompt')) + # create a mesh + mesh = make_pyramid_mesh() + print("Updating the color per vertex channel") + rt.setNumCPVVerts(mesh, 2) + rt.buildVCFaces(mesh) + rt.setVertColor(mesh, 1, rt.Color(255, 0, 0)) + rt.setVertColor(mesh, 2, rt.Color(0, 0, 255)) + rt.setVCFace(mesh, 1, rt.Point3(1, 1, 2)) + rt.setVCFace(mesh, 2, rt.Point3(1, 2, 2)) + rt.setVCFace(mesh, 3, rt.Point3(2, 2, 2)) + rt.setVCFace(mesh, 4, rt.Point3(1, 1, 1)) + rt.setCVertMode(mesh, True) + rt.update(mesh) + output_channels(mesh) + +main() diff --git a/src/samples/pymxs/mxs_token.py b/src/samples/pymxs/mxs_token.py new file mode 100644 index 0000000..0fb37e6 --- /dev/null +++ b/src/samples/pymxs/mxs_token.py @@ -0,0 +1,90 @@ +''' + Demonstrates the use of mxstoken +''' +import threading +import time +import pymxs # pylint: disable=import-error + +def mxstoken_sample(): + ''' + Demonstrate 3 threads using mxstoken. + ''' + flag = True + counter = 0 + + def call_mxs_entry(): + ''' + Access pymxs in a mxstoken protected block of code. + ''' + with pymxs.mxstoken(): + pymxs.runtime.Teapot() + + def call_mxs_entry_ex_1(locker, tick, evt): + ''' + Demonstrate a first function with a mix of concurrent code + and mxstoken protected code. + ''' + try: + locker.acquire() + nonlocal flag, counter + flag = False + with pymxs.mxstoken(): + pymxs.runtime.Teapot(Name="call_mxs_entry_ex_1") + # give up lock, let ex_2 could exec codes + locker.release() + if not evt.wait(tick): + pymxs.print_( + "Error: event untriggered\n" + + "which indicates 'with block' in ex_2 haven't finished\n", + True, + True) + counter = 30 + except: + pymxs.print_("Error: unexpected exception\n", True, True) + raise + finally: + if locker.locked(): + locker.release() + + def call_mxs_entry_ex_2(locker, tick, evt): + ''' + Demonstrate a second function with a mix of concurrent code + and mxstoken protected code. + ''' + nonlocal flag, counter + while flag: + time.sleep(tick) + + try: + locker.acquire() + # we expected this block is finished + # before ex_1 wakeup from sleep + for _ in range(10): + # only a indicator, could just assign counter = 10 + counter = counter + 1 + evt.set() + with pymxs.mxstoken(): + # this block won't be executed after ex_1 with block finished + pymxs.runtime.Teapot(Name="call_mxs_entry_ex_2") + if counter != 30: + pymxs.print_( + ("Error: expected counter 30, got %d" + + "which indicates 'with block' in ex_2 haven't finished\n").format(counter), + True, + True) + finally: + if locker.locked(): + locker.release() + pymxs.print_("success\n", False, True) + + # Steps: + locker = threading.Lock() + evt = threading.Event() + thread1 = threading.Thread(target=call_mxs_entry) + thread2 = threading.Thread(target=call_mxs_entry_ex_1, args=(locker, 1, evt)) + thread3 = threading.Thread(target=call_mxs_entry_ex_2, args=(locker, 0.01, evt)) + thread1.start() + thread2.start() + thread3.start() + +mxstoken_sample() diff --git a/src/samples/pymxs/notifications.py b/src/samples/pymxs/notifications.py new file mode 100644 index 0000000..2dd78fa --- /dev/null +++ b/src/samples/pymxs/notifications.py @@ -0,0 +1,303 @@ +''' + Lists all of the notification codes broadcast by 3ds Max, + and registers a callback function for each and every one. +''' +import os +from pymxs import runtime as rt # pylint: disable=import-error + +# sadly, maxscript does not expose this list +NOTIFICATIONS = [ + "unitsChange", + "timeunitsChange", + "viewportChange", + "spacemodeChange", + "systemPreReset", + "systemPostReset", + "systemPreNew", + "systemPostNew", + "filePreOpen", + "filePostOpen", + "filePreMerge", + "filePostMerge", + "filePreSave", + "filePostSave", + "selectionSetChanged", + "bitmapChanged", + "preRender", + "preRenderFrame", + "postRender", + "postRenderFrame", + "preImport", + "postImport", + "importFailed", + "preExport", + "postExport", + "exportFailed", + "nodeRenamed", + "modPanelSelChanged", + "animateOn", + "animateOff", + "mtlLibPreOpen", + "mtlLibPostOpen", + "mtlLibPreSave", + "mtlLibPostSave", + "mtlLibPreMerge", + "mtlLibPostMerge", + "preRenderEval", + "renderParamsChanged", + "nodeCreated", + "nodeLinked", + "nodeUnlinked", + "nodeHide", + "nodeUnhide", + "nodeFreeze", + "nodeUnfreeze", + "nodePreMaterial", + "nodePostMaterial", + "sceneNodeAdded", + "selectedNodesPreDelete", + "selectedNodesPostDelete", + "mainWindowEnabled", + "preSystemShutdown", + "postSystemStartup", + "pluginLoaded", + "postSystemShutdown", + "colorChanged", + "heightMenuChanged", + "fileLinkPreBind", + "fileLinkPostBind", + "fileLinkPreDetatch", + "fileLinkPostDetatch", + "fileLinkPreReload", + "fileLinkPostReload", + "fileLinkPreAttach", + "fileLinkPostAttach", + "nodePreDelete", + "nodePostDelete", + "radiosityProcessStart", + "radiosityProcessStopped", + "radiosityProcessReset", + "radiosityProcessDone", + "modPanelObjPreChange", + "modPanelObjPostChange", + "sceneUndo", + "sceneRedo", + "manipulateModeOn", + "manipulateModeOff", + "animationRangeChange", + "filePostMergeProcess", + "filePostOpenProcess", + "svSelectionSetChanged", + "svDoubleClickGraphNode", + "preModifierAdded", + "postModifierAdded", + "preModifierDeleted", + "postModifierDeleted", + "postNodesCloned", + "preRendererChange", + "postRendererChange", + "svPreLayoutChange", + "svPostLayoutChange", + "layerCreated", + "layerDeleted", + "nodeLayerChanged", + "beginRenderingActualFrame", + "beginRenderingReflectRefractMap", + "beginRenderingTonemappingImage", + "byCategoryDisplayFilterChanged", + "customDisplayFilterChanged", + "filePreSaveOld", + "filePostSaveOld", + "filelinkPostReloadPrePrune", + "lightingUnitDisplaySystemChange", + "mtlRefAdded", + "mtlRefDeleted", + "nodeCloned", + "objectXrefPreMerge", + "objectXrefPostMerge", + "preMirrorNodes", + "postMirrorNodes", + "preNodeBonePropChanged", + "postNodeBonePropChanged", + "preNodeGeneralPropChanged", + "postNodeGeneralPropChanged", + "preNodeGiPropChanged", + "postNodeGiPropChanged", + "preNodeMentalrayPropChanged", + "postNodeMentalrayPropChanged", + "preNodeUserPropChanged", + "postNodeUserPropChanged", + "preProgress", + "postProgress", + "preNodesCloned", + "radiosityPluginChanged", + "sceneXrefPostMerge", + "sceneXrefPreMerge", + "systemPostDirChange", + "systemPreDirChange", + "tabbedDialogCreated", + "tabbedDialogDeleted", + "nodeNameSet", + "preSceneUndo", + "preSceneRedo", + "preSceneStateSave", + "postSceneStateSave", + "preSceneStateRestore", + "postSceneStateRestore", + "sceneStateDelete", + "sceneStateRename", + "filePreOpenProcess", + "filePreSaveProcess", + "filePostSaveProcess", + "classDescLoaded", + "atsPreRepathPhase", + "atsPostRepathPhase", + "proxyTempDisableStart", + "proxyTempDisableEnd", + "NamedSelSetCreated", + "NamedSelSetDeleted", + "NamedSelSetRenamed", + "ModPanelSubObjectLevelChanged", + "FailedDirectXMaterialTextureLoad", + "D3DPreDeviceReset", + "D3DPostDeviceReset", + "postSceneReset", + "animLayersEnabled", + "animLayersDisabled", + "selectionLocked", + "selectionUnlocked", + "preImageViewerDisplay", + "postImageViewerDisplay", + "imageViewerUpdate", + "activeViewportChanged", + "NamedSelSetPreModify", + "NamedSelSetPostModify", + "ClassDescAdded", + "ObjectDefinitionChangeBegin", + "ObjectDefinitionChangeEnd", + "preAppThemeChange", + "postAppThemeChange", + "preViewPanelDelete", + "preWorkspaceChange", + "postWorkspaceChange", + "preWorkspaceCollectionChange", + "postWorkspaceCollectionChange", + "mouseSettingsChanged", + "preSavingCuiToolbars", + "postSavingCuiToolbars", + "preLoadingCuiToolbars", + "postLoadingCuiToolbars", + "appActivated", + "appDeactivated", + "cuiMenusUpdate", + "fileOpenFailed", + "postRestoreObjsDeleted", + "preSavingMenus", + "postSavingMenus", + "viewportSafeFrameToggle", + "postLoadingMenus", + "layerParentChanged", + "actionItemHotkeyPreExecute", + "actionItemHotKeyPostExecute", + "actionItemExecutionStarted", + "actionItemExecutionEnded", + "interactivePluginCreationStarted", + "interactivePluginCreationEnded", + "filePostMerge2", + "postNodeSelectOperation", + "preViewportTooltip", + "welcomeScreenDone", + "playbackStart", + "playbackEnd", + "sceneExplorerNeedsUpdate", + "filePostOpenProcessFinalized", + "filePostMergeProcessFinalized", + "preProjectFolderChange", + "postProjectFolderChange", + "preStartupScriptLoad", + "activeShadeInViewportToggled", + "systemShutdownCheck", + "systemShutdownCheckFailed", + "systemShutdownCheckPassed", + "filePostMerge3", + "matLibPreOpen", + "matLibPostOpen", + "matLibPreSave", + "matLibPostSave", + "matLibPreMerge", + "matLibPostMerge", + "selNodesPreDelete", + "selNodesPostDelete", + "wmEnable"] + +def list_codes(): + """List all known notification names.""" + print(NOTIFICATIONS) + print(f"Number Notifications registered: {len(NOTIFICATIONS)}") + +def handle_notification(code): + """Handle a specific notification.""" + print(f"Notification handled: {code}") + +def handle_callback(): + """Generic callback Python handler.""" + # note: less generic callback function shall be defined for specific events + # the notificationParams() returned elements are specific to event name/type + print(f"Received event notification\n with notification parameters being {rt.callbacks.notificationParam()}") + +def create_maxscript_callback_function(): + """Register a maxscript function to a Python callcack.""" + # Create a maxscript function (called 'pcb') referencing a Python function + rt.pcb = handle_notification + +def register_all_callbacks(): + """Register all the callbacks that we know.""" + create_maxscript_callback_function() + for name in NOTIFICATIONS: + # register a maxscript line to call for specified event name + # calls the referenced Python function with an hardcoded argument + # the optional named 'id' argument is used as a best practice, + # to make it easier to find and remove callbacks later + rt.callbacks.addScript(rt.Name(name), "pcb(\"{}\")".format(name), id=rt.Name("my_mxs_handler")) + + # register a Python function to call for event name + # the registered function cannot take any arguments + # the optional named 'id' argument is used as a best practice, + # to make it easier to find and remove callbacks later + rt.callbacks.addScript(rt.Name(name), handle_callback, id=rt.Name("my_python_handler")) + +def unregister_all_callbacks(): + """Unregister all the callbacks that we know.""" + for name in NOTIFICATIONS: + rt.callbacks.removeScripts(rt.Name(name), id=rt.Name("my_mxs_handler")) + rt.callbacks.removeScripts(rt.Name(name), id=rt.Name("my_python_handler")) + +def main(): + """Demonstrate callback registration.""" + # List all callback names that we know + list_codes() + # Register all callback names + register_all_callbacks() + + # Do some things + print("Creating sphere") + sphere1 = rt.sphere() + print("Setting radius for sphere 1") + sphere1.radius = 2.0 + print("Creating sphere 2") + sphere2 = rt.sphere() + print("Setting radius on sphere 2") + sphere2.radius = 2.0 + print("Setting parent of node 2 to node 1") + sphere2.Parent = sphere1 + + print("Saving file") + output_path = os.path.join(rt.sysInfo.tempdir, 'temp.max') + rt.saveMaxFile(output_path) + print("Opening file") + rt.loadMaxFile(output_path) + + print('unregistering notification handlers') + unregister_all_callbacks() + +main() diff --git a/src/samples/pymxs/output_plugin_classes.py b/src/samples/pymxs/output_plugin_classes.py new file mode 100644 index 0000000..9407a33 --- /dev/null +++ b/src/samples/pymxs/output_plugin_classes.py @@ -0,0 +1,14 @@ +''' + Demonstrates using the PluginManager to extract information about loaded + plugins. +''' +from pymxs import runtime as rt # pylint: disable=import-error + +# List all plug-in dlls +PLUGIN_COUNT = rt.pluginManager.pluginDllCount +print(f"Total PluginDlls: {PLUGIN_COUNT}\n") +# maxscript uses one based indices +for p in range(1, PLUGIN_COUNT + 1): + print("PluginDll:", rt.pluginManager.pluginDllFullPath(p)) + print("Description:", rt.pluginManager.pluginDllName(p)) + print("Loaded:", rt.pluginManager.isPluginDllLoaded(p)) diff --git a/src/samples/pymxs/poly_object.py b/src/samples/pymxs/poly_object.py new file mode 100644 index 0000000..fd33c81 --- /dev/null +++ b/src/samples/pymxs/poly_object.py @@ -0,0 +1,42 @@ +''' + Demonstrates how to create a mmesh from scratch and to set color per vertex data. +''' +from pymxs import runtime as rt # pylint: disable=import-error + +def make_pyramid_mesh(side=20.0): + '''Construct a pyramid from vertices and faces.''' + halfside = side / 2.0 + return rt.mesh( + vertices=[ + rt.point3(0.0, 0.0, side), + rt.point3(-halfside, -halfside, 0.0), + rt.point3(-halfside, halfside, 0.0), + rt.point3(halfside, 0.0, 0.0) + ], + faces=[ + rt.point3(1, 2, 3), + rt.point3(1, 3, 4), + rt.point3(1, 4, 2), + rt.point3(2, 3, 4), + ]) + +def color_pyramid_mesh(mesh): + '''Add two color vertices, and refer them in the faces (color the pyramid).''' + rt.setNumCPVVerts(mesh, 2, True) + rt.setVertColor(mesh, 1, rt.Point3(255, 0, 0)) + rt.setVertColor(mesh, 2, rt.Point3(0, 0, 255)) + rt.buildVCFaces(mesh) + rt.setVCFace(mesh, 1, 1, 1, 2) + rt.setVCFace(mesh, 2, 1, 2, 2) + rt.setVCFace(mesh, 3, 2, 2, 2) + rt.setVCFace(mesh, 4, 1, 1, 1) + rt.setCVertMode(mesh, True) + rt.update(mesh) + +def main(): + '''Construct a pyramid and then add colors to its faces.''' + rt.resetMaxFile(rt.Name('noPrompt')) + mesh = make_pyramid_mesh() + color_pyramid_mesh(mesh) + +main() diff --git a/src/samples/pymxs/pymxs_classes.py b/src/samples/pymxs/pymxs_classes.py new file mode 100644 index 0000000..e97a309 --- /dev/null +++ b/src/samples/pymxs/pymxs_classes.py @@ -0,0 +1,37 @@ +''' + Demonstrates using the inspect module to list all of the classes in + pymxs and the total number of members exposed. + + (Note that there is no 1:1 relationship between the classes in + maxscript and the python classes exposed in pymxs: a single pymxs + wrapper exposes almost all maxscript classes. You can use + pymxs.runtime.apropos(), showClass(), and other MAXScript inspection + methods to get information about MAXScript objects and classes) +''' +import os +import inspect +import pymxs # pylint: disable=import-error +from pymxs import runtime as rt # pylint: disable=import-error + +def inspect_pymxs(): + """Inspect the classes exported by pymxs and generate a report.""" + api = {} + classes = inspect.getmembers(pymxs, inspect.isclass) + totalcnt = 0 + for curclass in classes: + name = str(curclass[0]) + membercnt = len(curclass[1].__dict__) + totalcnt += membercnt + api[name] = membercnt + + fname = os.path.join(rt.sysInfo.tempdir, 'pyms_api.txt') + with open(fname, 'w') as output: + for k in sorted(api.keys()): + output.write(k + " has " + str(api[k]) + " members\n") + + print("Results saved to", fname) + print("Total number of classes ", len(api)) + print("Total number of API elements ", totalcnt) + print("Average number of API elements per class ", totalcnt / len(api)) + +inspect_pymxs() diff --git a/src/samples/pymxs/render.py b/src/samples/pymxs/render.py new file mode 100644 index 0000000..2242b80 --- /dev/null +++ b/src/samples/pymxs/render.py @@ -0,0 +1,63 @@ +""" + Demonstrate scene rendering with pymxs. +""" +import os +import math +import pymxs # pylint: disable=import-error +from pymxs import runtime as rt # pylint: disable=import-error + +INST = rt.Name("instance") + +def create_spheres(): + '''Create a scene made of spiralling spheres.''' + sphere = rt.sphere(radius=6.0) + revolutions = 9 * 360 + radius = 40.0 + z_sphere = 0.0 + # cloning the original sphere to create the spiral effect + for i in range(0, revolutions, 20): + # the maxscript CloneNodes method accepts a named argument called 'newNodes' + # the argument must be sent by reference as it serves as an output argument + # since the argument is not also an input argument, we can simply initialize + # the byref() object as 'None' + # the output argument along with the call result is then returned in a tuple + # note: 'newNodes' returns an array of cloned nodes + # in the current case, only one element is cloned + result, nodes = rt.MaxOps.CloneNodes(sphere, cloneType=INST, newNodes=pymxs.byref(None)) + radians = math.radians(i) + x_sphere = radius * math.cos(radians) + y_sphere = radius * math.sin(radians) + # note: 'newNodes' returned an array of cloned nodes + # in the current case, only one element is cloned + nodes[0].Position = rt.Point3(x_sphere, y_sphere, z_sphere) + z_sphere += 1.0 + radius -= 0.20 + +def maximize_perspective(): + '''Setup perspective for the render''' + rt.viewport.setLayout(rt.Name('layout_1')) + rt.viewport.setType(rt.Name('view_persp_user')) + rt.viewport.setTM( + rt.matrix3( + rt.point3(0.707107, 0.353553, -0.612372), + rt.point3(-0.707107, 0.353553, -0.612372), + rt.point3(0, 0.866025, 0.5), + rt.point3(-0.00967026,-70.3466,-552.481) + ) + ) + +def render(): + '''Render in the renderoutput directory.''' + output_path = os.path.join(rt.getDir(rt.Name("renderoutput")), 'foo.jpg') + if os.path.exists(output_path): + os.remove(output_path) + rt.render(outputFile=output_path) + +def demo_render(): + '''Create a demo scene, adjust the perspective and render the scene''' + rt.resetMaxFile(rt.Name('noPrompt')) + create_spheres() + maximize_perspective() + render() + +demo_render() diff --git a/src/samples/pymxs/scene_graph.py b/src/samples/pymxs/scene_graph.py new file mode 100644 index 0000000..ed94617 --- /dev/null +++ b/src/samples/pymxs/scene_graph.py @@ -0,0 +1,12 @@ +''' + Creates a simple text representation of the scene graph +''' +from pymxs import runtime as rt # pylint: disable=import-error + +def output_node(node, indent=''): + """Print the scene graph as text to stdout.""" + print(indent, node.Name) + for child in node.Children: + output_node(child, indent + '--') + +output_node(rt.rootnode) diff --git a/src/samples/pymxs/sphere_borg.py b/src/samples/pymxs/sphere_borg.py new file mode 100644 index 0000000..7c564ae --- /dev/null +++ b/src/samples/pymxs/sphere_borg.py @@ -0,0 +1,23 @@ +''' + Demonstrates creating objects, object instancing, and object translation. +''' +from pymxs import runtime as rt # pylint: disable=import-error + +INST = rt.Name("instance") + +def create_borg(obj, num, spacing): + """Create a bunch of clones of the provided object""" + for i in range(num): + for j in range(num): + for k in range(num): + if i or j or k: + point = rt.Point3(i * spacing, j * spacing, k * spacing) + rt.MaxOps.CloneNodes(obj, cloneType=INST, offset=point) + +def main(): + """Create a base object and turn it into a borg, whatever that is.""" + obj = rt.sphere() + obj.Radius = 2.0 + create_borg(obj, 4, 5.0) + +main() diff --git a/src/samples/pymxs/timer.py b/src/samples/pymxs/timer.py new file mode 100644 index 0000000..c87af06 --- /dev/null +++ b/src/samples/pymxs/timer.py @@ -0,0 +1,64 @@ +""" + Demonstrate timers. +""" +import threading +import time +import pymxs # pylint: disable=import-error + +GLOBAL_ARGS = { + "nativeCallTagA": 1, + "nativeCallTagB": "", + "nativeCallWithWrapperObjTagA": 1, + "nativeCallWithWrapperObjName": u"TestName", + "wrapObj": None +} + +def native_call(first, second, local_env): + """Run timer payload""" + local_env["nativeCallTagA"] = first + local_env["nativeCallTagB"] = second + +def native_with_wrapper(first, wrap_obj, local_env): + """Run timer payload""" + local_env["nativeCallWithWrapperObjTagA"] = first + wrap_obj.Name = local_env["nativeCallWithWrapperObjName"] + local_env["wrapObj"] = wrap_obj + +def check(local_env): + """Validate expected results""" + is_success = True + if local_env["nativeCallTagA"] != 10 or local_env["nativeCallTagB"] != "second": + is_success = False + print("Error: Incorrect native call for threading timer") + + wrap_teapot = local_env["wrapObj"] + if (local_env["nativeCallWithWrapperObjTagA"] != 10 or + wrap_teapot is None or + wrap_teapot.Name != local_env["nativeCallWithWrapperObjName"]): + is_success = False + print("Error: Incorrect native call with wrapper object for threading timer") + + return is_success + +def main(): + """Demonstrate timers""" + wrap_teapot = pymxs.runtime.Teapot() + + # test native call + native_call_timer = threading.Timer( + 0.1, + native_call, + [10, "second"], + kwargs={"local_env": GLOBAL_ARGS}) + native_with_wrapper_timer = threading.Timer( + 0.1, + native_with_wrapper, + kwargs={"first": 10, "wrap_obj": wrap_teapot, "local_env": GLOBAL_ARGS}) + print("Start timer") + native_call_timer.start() + native_with_wrapper_timer.start() + time.sleep(0.2) + if check(GLOBAL_ARGS): + print("threading timer success") + +main() diff --git a/src/samples/pymxs/transform_nodes.py b/src/samples/pymxs/transform_nodes.py new file mode 100644 index 0000000..6bfe74d --- /dev/null +++ b/src/samples/pymxs/transform_nodes.py @@ -0,0 +1,56 @@ +''' + Creates a number of boxes with random scale, position, and rotation. +''' +from random import random as rnd +import math +from pymxs import runtime as rt # pylint: disable=import-error + +def rnd_angle(): + """Return a random angle in radians.""" + return -math.pi + (rnd() * 2 * math.pi) + +def rnd_quat(): + """Return a random quaternion.""" + return rt.Quat(rnd(), rnd(), rnd(), rnd_angle()) + +def rnd_dist(): + """Return a random distance.""" + return rnd() * 100.0 - 50.0 + +def rnd_position(): + """Return a random position.""" + return rt.Point3(rnd_dist(), rnd_dist(), 0) + +def rnd_scale_amount(): + """Return a random scaling amount.""" + return rnd() * 2.0 + 0.1 + +def rnd_scale(): + """Return a random (x,y,z) scaling as a Point3.""" + return rt.Point3(rnd_scale_amount(), rnd_scale_amount(), rnd_scale_amount()) + +def random_transform_nodes(nodes): + """Apply a random transformation (scaling, rotation, position) + to a list of nodes.""" + for node in nodes: + node.Scaling = rnd_scale() + node.Rotation = rnd_quat() + node.Position = rnd_position() + +def create_nodes(count): + """Return count nodes.""" + def make_box(): + """Create a single node, a box of (10, 10, 10).""" + box = rt.box() + box.length = 10.0 + box.height = 10.0 + box.width = 10.0 + return box + return [make_box() for i in range(count)] + +def main(): + """Demonstrate the generation and transformation of 25 boxes.""" + nodes = create_nodes(25) + random_transform_nodes(nodes) + +main() diff --git a/src/samples/pymxs/tree_of_spheres.py b/src/samples/pymxs/tree_of_spheres.py new file mode 100644 index 0000000..f1424a5 --- /dev/null +++ b/src/samples/pymxs/tree_of_spheres.py @@ -0,0 +1,23 @@ +''' + Creates a hierarchy of sphere objects at different relative locations. +''' +from pymxs import runtime as rt # pylint: disable=import-error + +def create_sphere(): + """Create and return a single sphere of radius 5.""" + sphere = rt.sphere() + sphere.radius = 5 + return sphere + +def tree_of_spheres(parent, width, xinc, depth, maxdepth): + """Create a tree of spheres.""" + if depth == maxdepth: + return + for i in range(width): + sphere = create_sphere() + pos = parent.pos + sphere.pos = rt.Point3(pos.x + i * xinc, 0, pos.z + 15) + sphere.Parent = parent + tree_of_spheres(sphere, width, xinc * width, depth + 1, maxdepth) + +tree_of_spheres(create_sphere(), 2, 10, 0, 4) diff --git a/src/samples/unicode_io.py b/src/samples/unicode_io.py new file mode 100644 index 0000000..f789b93 --- /dev/null +++ b/src/samples/unicode_io.py @@ -0,0 +1,139 @@ +""" + Demonstrate file io using unicode paths and unicode content. +""" +import tempfile +import os +import codecs +import shutil + +# Strings for the file content +TEXT_STR = 'Text String: Hello!\n' +UNI_TEXT_STR = u'Unicode String: 女時代' + +# Get the current working folder +CURRENT_DIR = os.getcwd() + +# Create Unicode directory name +UNI_DIR = u'時' + +# Set our user folder to the user temp folder +TEMP_DIR = tempfile.gettempdir() + +# Create Unicode file name +UNI_FILE = u'Pÿ x Mxs.txt' + +# Set our temp folder plus the Unicode directory +FULL_PATH = TEMP_DIR + '\\' + UNI_DIR + +# Set our filename +F_NAME = UNI_FILE + +def create_uni_dir(): + """Create a directory with a unicode name.""" + # Remove directory if it already exists + if os.path.exists(FULL_PATH): + remove_uni_dir() + try: + # Make sure we are in the correct directory root + os.chdir(TEMP_DIR) + print('Working Directory:\n ' + os.getcwd()) + except IOError: + print('!FAIL! Could not set working directory!\n') + else: + print('Moved to Temp folder:\n ' + os.getcwd()) + + try: + # Make our directory + os.mkdir(FULL_PATH) + except IOError: + print('FAIL! Could not create unicode directory:\n' + FULL_PATH) + else: + print('Created unicode directory:\n' + FULL_PATH) + +def remove_uni_dir(): + """Remove a directory with a unicode name.""" + # Check if the directory exists + if os.path.exists(FULL_PATH): + try: + # Change to our working folder to be safe + os.chdir(TEMP_DIR) + print('Working Directory:\n ' + os.getcwd()) + except IOError: + print('!FAIL! Directory does not exist!\n') + else: + # Since we know we are in our working folder, remove the Unicode + # directory created my createDir() + shutil.rmtree(UNI_DIR) + print('Removed unicode directory:\n' + FULL_PATH) + +def open_file(): + """Open a file in working directory and write in it.""" + # Change to our working folder to be safe + os.chdir(TEMP_DIR) + # Set up our file and set it's encoding to UTF-8 + with codecs.open(F_NAME, encoding='utf-8', mode='w+') as thefile: + # Write to our file (this could be done as a try) + thefile.write(TEXT_STR + UNI_TEXT_STR) + print('Finished writing file to ' + F_NAME) + # Close our file + thefile.close() + +def open_file_in_uni_dir(): + """Open a file in unicode directory and write in it.""" + # Change to our working folder to be safe + os.chdir(FULL_PATH) + # Set up our file and set it's encoding to UTF-8 + with codecs.open(F_NAME, encoding='utf-8', mode='w+') as thefile: + # Write to our file (this could be done as a try) + thefile.write(TEXT_STR + UNI_TEXT_STR) + print('Finished writing file to ' + FULL_PATH + F_NAME) + # Close our file + thefile.close() + +def remove_uni_file(): + """Remove a unicode file.""" + # Change to our working folder to be safe + os.chdir(TEMP_DIR) + # Check if the file exists + if os.path.exists(TEMP_DIR + F_NAME): + print('File ' + F_NAME + ' exists and will be removed!') + try: + # Remove our file + os.remove(TEMP_DIR + F_NAME) + except IOError: + print('!FAIL! - File not deleted') + else: + print('File Removed.') + +# Create some setup stats for output +try: + STATS = unicode( + 'Setup:\n' + + 'Current directory: ' + + CURRENT_DIR + + '\nOutput filename: ' + + UNI_FILE + + '\nFile contents: ' + + TEXT_STR + + UNI_TEXT_STR) +except NameError: + STATS = ( + 'Setup:\n' + + 'Current directory: ' + + CURRENT_DIR + + '\nOutput filename: ' + + UNI_FILE + + '\nFile contents: ' + + TEXT_STR + + UNI_TEXT_STR) +# Output stats +print(STATS) + +# Run our functions +open_file() +create_uni_dir() +open_file_in_uni_dir() +# Comment these out to leave written files and created directory +# to visually verify files and files content +remove_uni_dir() +remove_uni_file() diff --git a/uninstall.sh b/uninstall.sh index 488f23d..7628524 100644 --- a/uninstall.sh +++ b/uninstall.sh @@ -1,7 +1,7 @@ #! /usr/bin/env bash set -e script=$(dirname $(readlink -f "$0")) -source "$script/inst.sh" +source "$script/scripts/inst.sh" # make sure we have 3ds Max in the current path if [ ! -f ./3dsmax.exe ] diff --git a/uninstallhowtos.sh b/uninstallhowtos.sh index 5b8b7b0..4f783a7 100644 --- a/uninstallhowtos.sh +++ b/uninstallhowtos.sh @@ -1,12 +1,12 @@ #! /usr/bin/env bash set -e script=$(dirname $(readlink -f "$0")) -source "$script/inst.sh" +source "$script/scripts/inst.sh" venvscript () { echo "cd Scripts" echo "call activate.bat" - for f in $(find "$script" -name "setup.py") + for f in $(find "$packagedir" -name "setup.py") do local package=$(basename "$(dirname "$f")") echo "pip.exe uninstall -y $package-autodesk"