├── src
├── samples
│ ├── __init__.py
│ ├── PySide
│ │ ├── cylinder_icon_48.png
│ │ ├── ui_loader.py
│ │ ├── simple_dialog.py
│ │ ├── combine_meshes.py
│ │ ├── test_ui.ui
│ │ └── docking_widgets.py
│ ├── pymxs
│ │ ├── file_save.py
│ │ ├── list_scripts.py
│ │ ├── assets.py
│ │ ├── scene_graph.py
│ │ ├── bent_cylinder.py
│ │ ├── enumerate_parameters.py
│ │ ├── output_plugin_classes.py
│ │ ├── hit_test.py
│ │ ├── get_rendered_normals.py
│ │ ├── sphere_borg.py
│ │ ├── tree_of_spheres.py
│ │ ├── combine_selected_meshes.py
│ │ ├── apply_material.py
│ │ ├── poly_object.py
│ │ ├── make_instances.py
│ │ ├── pymxs_classes.py
│ │ ├── transform_nodes.py
│ │ ├── app_chunk.py
│ │ ├── timer.py
│ │ ├── animation.py
│ │ ├── render.py
│ │ ├── materials.py
│ │ ├── mxs_token.py
│ │ ├── mesh_and_cpv.py
│ │ └── class_types.py
│ └── unicode_io.py
├── packages
│ ├── mxthread
│ │ ├── LICENSE
│ │ ├── setup.py
│ │ └── mxthread
│ │ │ └── __init__.py
│ ├── mxvscode
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── setup.py
│ │ ├── mxvscode
│ │ │ └── __init__.py
│ │ └── LICENSE
│ ├── mxstranslate
│ │ ├── LICENSE
│ │ ├── doc
│ │ │ └── translate.png
│ │ ├── README.md
│ │ ├── setup.py
│ │ └── mxstranslate
│ │ │ └── __init__.py
│ ├── pyconsole
│ │ ├── LICENSE
│ │ ├── doc
│ │ │ └── pyconsole.png
│ │ ├── setup.py
│ │ ├── pyconsole
│ │ │ ├── __init__.py
│ │ │ └── console.py
│ │ └── README.md
│ ├── socketioclient
│ │ ├── LICENSE
│ │ ├── setup.py
│ │ └── socketioclient
│ │ │ └── __init__.py
│ ├── mxs2py
│ │ ├── doc
│ │ │ ├── mxs2py.png
│ │ │ └── mxs2pyout.png
│ │ ├── mxs2py
│ │ │ ├── __init__.py
│ │ │ ├── log.py
│ │ │ ├── mxscp.py
│ │ │ ├── mxs2py.py
│ │ │ ├── limitations.py
│ │ │ └── syntax.py
│ │ ├── setup.py
│ │ ├── LICENSE
│ │ └── README.md
│ ├── reloadmod
│ │ ├── Capture.png
│ │ ├── README.md
│ │ ├── setup.py
│ │ ├── reloadmod
│ │ │ ├── __init__.py
│ │ │ └── reload.py
│ │ └── LICENSE
│ ├── quickpreview
│ │ ├── doc
│ │ │ └── Preview.png
│ │ ├── setup.py
│ │ ├── LICENSE
│ │ ├── quickpreview
│ │ │ └── __init__.py
│ │ └── README.md
│ ├── renameselected
│ │ ├── doc
│ │ │ └── Dialog.png
│ │ ├── setup.py
│ │ ├── renameselected
│ │ │ ├── __init__.py
│ │ │ └── ui.py
│ │ ├── LICENSE
│ │ └── README.md
│ ├── speedsheet
│ │ ├── doc
│ │ │ └── Speedsheet.png
│ │ ├── setup.py
│ │ ├── LICENSE
│ │ ├── speedsheet
│ │ │ └── __init__.py
│ │ └── README.md
│ ├── zdepthchannel
│ │ ├── doc
│ │ │ └── ZDepth.png
│ │ ├── setup.py
│ │ ├── LICENSE
│ │ ├── zdepthchannel
│ │ │ └── __init__.py
│ │ └── README.md
│ ├── threadprogressbar
│ │ ├── doc
│ │ │ └── Progress.png
│ │ ├── setup.py
│ │ ├── threadprogressbar
│ │ │ ├── __init__.py
│ │ │ └── ui.py
│ │ ├── LICENSE
│ │ └── README.md
│ ├── menuhook
│ │ ├── setup.py
│ │ ├── LICENSE
│ │ └── README.md
│ ├── inbrowserhelp
│ │ ├── setup.py
│ │ ├── LICENSE
│ │ ├── README.md
│ │ └── inbrowserhelp
│ │ │ └── __init__.py
│ ├── transformlock
│ │ ├── setup.py
│ │ ├── transformlock
│ │ │ └── __init__.py
│ │ ├── LICENSE
│ │ └── README.md
│ ├── removeallmaterials
│ │ ├── setup.py
│ │ ├── removeallmaterials
│ │ │ └── __init__.py
│ │ ├── LICENSE
│ │ └── README.md
│ └── singleinstancedlg
│ │ ├── setup.py
│ │ ├── singleinstancedlg
│ │ ├── __init__.py
│ │ └── ui.py
│ │ ├── LICENSE
│ │ └── README.md
├── pystartup
│ ├── pystartup.ms
│ └── README.md
└── adn-devtech-python-howtos
│ ├── PackageContents.xml
│ └── scripts
│ └── pyStartup.py
├── .gitignore
├── .gitattributes
├── doc
├── Splash.png
├── Integration.png
├── uninstall.md
├── pluginpackage.md
└── install.md
├── scripts
├── makepreview.sh
├── create.sh
├── checks.sh
└── inst.sh
├── installstartup.sh
├── install.sh
├── .github
└── workflows
│ └── checks-workflow.yml
├── uninstall.sh
├── LICENSE
├── uninstallhowtos.sh
├── installhowtos.sh
└── README.md
/src/samples/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 | *.egg-info
3 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 | *.py text eol=lf
3 | *.sh text eol=lf
4 |
--------------------------------------------------------------------------------
/src/packages/mxthread/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2020 Autodesk, all rights reserved.
2 |
--------------------------------------------------------------------------------
/src/packages/mxvscode/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 | mxvscode_autodesk.egg-info/*
3 |
--------------------------------------------------------------------------------
/src/packages/mxstranslate/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2020 Autodesk, all rights reserved.
2 |
--------------------------------------------------------------------------------
/src/packages/pyconsole/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2020 Autodesk, all rights reserved.
2 |
--------------------------------------------------------------------------------
/src/packages/socketioclient/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2020 Autodesk, all rights reserved.
2 |
--------------------------------------------------------------------------------
/doc/Splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ADN-DevTech/3dsMax-Python-HowTos/HEAD/doc/Splash.png
--------------------------------------------------------------------------------
/doc/Integration.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ADN-DevTech/3dsMax-Python-HowTos/HEAD/doc/Integration.png
--------------------------------------------------------------------------------
/src/packages/mxs2py/doc/mxs2py.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ADN-DevTech/3dsMax-Python-HowTos/HEAD/src/packages/mxs2py/doc/mxs2py.png
--------------------------------------------------------------------------------
/src/packages/reloadmod/Capture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ADN-DevTech/3dsMax-Python-HowTos/HEAD/src/packages/reloadmod/Capture.png
--------------------------------------------------------------------------------
/src/packages/mxs2py/doc/mxs2pyout.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ADN-DevTech/3dsMax-Python-HowTos/HEAD/src/packages/mxs2py/doc/mxs2pyout.png
--------------------------------------------------------------------------------
/src/packages/mxs2py/mxs2py/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | mxs2py: convert maxscript code to python
3 | """
4 | from .mxs2py import preprocess, topy
5 |
--------------------------------------------------------------------------------
/src/packages/pyconsole/doc/pyconsole.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ADN-DevTech/3dsMax-Python-HowTos/HEAD/src/packages/pyconsole/doc/pyconsole.png
--------------------------------------------------------------------------------
/src/samples/PySide/cylinder_icon_48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ADN-DevTech/3dsMax-Python-HowTos/HEAD/src/samples/PySide/cylinder_icon_48.png
--------------------------------------------------------------------------------
/src/packages/quickpreview/doc/Preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ADN-DevTech/3dsMax-Python-HowTos/HEAD/src/packages/quickpreview/doc/Preview.png
--------------------------------------------------------------------------------
/src/packages/renameselected/doc/Dialog.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ADN-DevTech/3dsMax-Python-HowTos/HEAD/src/packages/renameselected/doc/Dialog.png
--------------------------------------------------------------------------------
/src/packages/speedsheet/doc/Speedsheet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ADN-DevTech/3dsMax-Python-HowTos/HEAD/src/packages/speedsheet/doc/Speedsheet.png
--------------------------------------------------------------------------------
/src/packages/zdepthchannel/doc/ZDepth.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ADN-DevTech/3dsMax-Python-HowTos/HEAD/src/packages/zdepthchannel/doc/ZDepth.png
--------------------------------------------------------------------------------
/src/packages/mxstranslate/doc/translate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ADN-DevTech/3dsMax-Python-HowTos/HEAD/src/packages/mxstranslate/doc/translate.png
--------------------------------------------------------------------------------
/src/packages/threadprogressbar/doc/Progress.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ADN-DevTech/3dsMax-Python-HowTos/HEAD/src/packages/threadprogressbar/doc/Progress.png
--------------------------------------------------------------------------------
/src/packages/mxvscode/README.md:
--------------------------------------------------------------------------------
1 | # mxsvscode
2 | VSCode debugging integration for 3ds Max.
3 |
4 | This package installs debugpy and enables debugging with vscode at the startup
5 | of 3ds Max.
6 |
--------------------------------------------------------------------------------
/src/packages/mxs2py/mxs2py/log.py:
--------------------------------------------------------------------------------
1 | """
2 | Simple logging utility (to stderr)
3 | """
4 | import sys
5 | def eprint(*args, **kwargs):
6 | """print to a file"""
7 | print(*args, file=sys.stderr, **kwargs)
8 |
--------------------------------------------------------------------------------
/scripts/makepreview.sh:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env bash
2 | set -e
3 | # create the Splash.png preview
4 | montage -size 400x400 **/**/*.png -thumbnail 300x300 +polaroid -resize 50% -gravity center -background none -extent 180x180 -background White -geometry -10+2 -tile x1 Splash.png
5 | echo "Done"
6 |
--------------------------------------------------------------------------------
/src/samples/pymxs/file_save.py:
--------------------------------------------------------------------------------
1 | """
2 | Demonstrate saving a 3ds Max file.
3 | """
4 | from os import path
5 | from pymxs import runtime as rt # pylint: disable=import-error
6 |
7 | FILEPATH = path.join(rt.sysInfo.tempdir, "test.max")
8 | rt.saveMaxFile(FILEPATH)
9 | print(f"Requested filename {FILEPATH}\n path {rt.maxFilePath}\n file {rt.maxFileName}")
10 |
--------------------------------------------------------------------------------
/src/samples/pymxs/list_scripts.py:
--------------------------------------------------------------------------------
1 | '''
2 | Lists all the files in a folder
3 | '''
4 | import os
5 | from pymxs import runtime as rt # pylint: disable=import-error
6 |
7 | PY_SCRIPTS_DIR = os.path.join(rt.getDir(rt.Name("scripts")), 'python')
8 | for root, dirs, files in os.walk(PY_SCRIPTS_DIR, topdown=False):
9 | for name in files:
10 | print(name)
11 |
--------------------------------------------------------------------------------
/src/packages/reloadmod/README.md:
--------------------------------------------------------------------------------
1 | # reloadmod
2 |
3 | 
4 |
5 | This pip package adds a menu item in 3ds Max that will reload
6 | all development pip packages in one operation.
7 |
8 |
9 | ## Installation
10 |
11 | ```bash
12 | cd $maxroot/Python37
13 | Python.exe -m pip install --user /path/to/reloadmod
14 | ```
15 |
16 | On the next 3ds Max startup the menu item will appear
17 |
18 |
--------------------------------------------------------------------------------
/src/samples/pymxs/assets.py:
--------------------------------------------------------------------------------
1 | '''
2 | Lists all of the assets in a file.
3 | '''
4 | from pymxs import runtime as rt # pylint: disable=import-error
5 |
6 | NASSETS = rt.AssetManager.GetNumAssets()
7 | print(f"There are {NASSETS} assets created")
8 | for i in range(NASSETS):
9 | a = rt.AssetManager.GetAssetByIndex(i + 1)
10 | print(f"Asset id = {a.GetAssetId()}, type = {a.getType()}, file = {a.getfilename()}")
11 |
--------------------------------------------------------------------------------
/src/samples/pymxs/scene_graph.py:
--------------------------------------------------------------------------------
1 | '''
2 | Creates a simple text representation of the scene graph
3 | '''
4 | from pymxs import runtime as rt # pylint: disable=import-error
5 |
6 | def output_node(node, indent=''):
7 | """Print the scene graph as text to stdout."""
8 | print(indent, node.Name)
9 | for child in node.Children:
10 | output_node(child, indent + '--')
11 |
12 | output_node(rt.rootnode)
13 |
--------------------------------------------------------------------------------
/installstartup.sh:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env bash
2 | set -e
3 | script="$(dirname "$(readlink -f "$0")")"
4 | source "$script/scripts/inst.sh"
5 |
6 | # make sure we have 3ds Max in the current path
7 | if [ ! -f ./3dsmax.exe ]
8 | then
9 | exiterr "This script must run in a 3ds Max installation directory."
10 | fi
11 |
12 | echo "Install pip if missing"
13 | installpip
14 |
15 | echo "Install pystartup"
16 | installpystartup
17 |
18 |
--------------------------------------------------------------------------------
/src/packages/menuhook/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | with open("README.md", "r") as fh:
4 | long_description = fh.read()
5 |
6 | setuptools.setup(
7 | name="menuhook-autodesk",
8 | version="0.0.1",
9 | description="Menu hook for 3ds Max",
10 | long_description=long_description,
11 | long_description_content_type="text/markdown",
12 | packages=setuptools.find_packages(),
13 | python_requires='>=3.7'
14 | )
15 |
--------------------------------------------------------------------------------
/src/samples/pymxs/bent_cylinder.py:
--------------------------------------------------------------------------------
1 | '''
2 | Demonstrates creating a cylinder and appling a bend modifier.
3 | '''
4 | from pymxs import runtime as rt # pylint: disable=import-error
5 |
6 | def main():
7 | """Create a cylinder and add a bend modifier to it."""
8 | cyl = rt.cylinder()
9 | cyl.radius = 10
10 | cyl.height = 30
11 | bend = rt.Bend()
12 | bend.bendAngle = 45
13 | rt.addModifier(cyl, bend)
14 |
15 | main()
16 |
--------------------------------------------------------------------------------
/install.sh:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env bash
2 | set -e
3 | script="$(dirname "$(readlink -f "$0")")"
4 | source "$script/scripts/inst.sh"
5 |
6 | # make sure we have 3ds Max in the current path
7 | if [ ! -f ./3dsmax.exe ]
8 | then
9 | exiterr "This script must run in a 3ds Max installation directory."
10 | fi
11 |
12 | echo "Install pip if missing"
13 | installpip
14 |
15 | echo "Install pystartup"
16 | installpystartup
17 |
18 | echo "install python packages"
19 | installpythonpackages
20 |
--------------------------------------------------------------------------------
/src/packages/mxstranslate/README.md:
--------------------------------------------------------------------------------
1 | # HowTo: mxstranslate
2 | 
3 |
4 | ## Experimental Translation Window (mxs -> Python)
5 |
6 | This project uses mxs2py to implement a dock widget
7 | containing a simple python editor. If you paste
8 | valid code in the editor window it gets translated
9 | to python.
10 |
11 | The translator is too slow for big chunks of code (and not
12 | always giving perfect result), but can be useful to get
13 | a Python version of maxscript snippet.
14 |
--------------------------------------------------------------------------------
/src/packages/speedsheet/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | with open("README.md", "r") as fh:
4 | long_description = fh.read()
5 |
6 | setuptools.setup(
7 | name="speedsheet-autodesk",
8 | version="0.0.1",
9 | description="Output Object Data to File",
10 | long_description=long_description,
11 | long_description_content_type="text/markdown",
12 | packages=setuptools.find_packages(),
13 | entry_points={'3dsMax': 'startup=speedsheet:startup'},
14 | python_requires='>=3.7'
15 | )
16 |
--------------------------------------------------------------------------------
/src/packages/inbrowserhelp/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | with open("README.md", "r") as fh:
4 | long_description = fh.read()
5 |
6 | setuptools.setup(
7 | name="inbrowserhelp-autodesk",
8 | version="0.0.1",
9 | description="inbrowserhelp sample",
10 | long_description=long_description,
11 | long_description_content_type="text/markdown",
12 | packages=setuptools.find_packages(),
13 | entry_points={'3dsMax': 'startup=inbrowserhelp:startup'},
14 | python_requires='>=3.7'
15 | )
16 |
--------------------------------------------------------------------------------
/src/packages/quickpreview/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | with open("README.md", "r") as fh:
4 | long_description = fh.read()
5 |
6 | setuptools.setup(
7 | name="quickpreview-autodesk",
8 | version="0.0.1",
9 | description="Create a quick preview",
10 | long_description=long_description,
11 | long_description_content_type="text/markdown",
12 | packages=setuptools.find_packages(),
13 | entry_points={'3dsMax': 'startup=quickpreview:startup'},
14 | python_requires='>=3.7'
15 | )
16 |
--------------------------------------------------------------------------------
/src/packages/zdepthchannel/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | with open("README.md", "r") as fh:
4 | long_description = fh.read()
5 |
6 | setuptools.setup(
7 | name="zdepthchannel-autodesk",
8 | version="0.0.1",
9 | description="Access the Z-Depth Channel",
10 | long_description=long_description,
11 | long_description_content_type="text/markdown",
12 | packages=setuptools.find_packages(),
13 | entry_points={'3dsMax': 'startup=zdepthchannel:startup'},
14 | python_requires='>=3.7'
15 | )
16 |
--------------------------------------------------------------------------------
/src/packages/reloadmod/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | with open("README.md", "r") as fh:
4 | long_description = fh.read()
5 |
6 | setuptools.setup(
7 | name="reloadmod-autodesk",
8 | version="0.0.1",
9 | description="Reload python modules during development",
10 | long_description=long_description,
11 | long_description_content_type="text/markdown",
12 | packages=setuptools.find_packages(),
13 | entry_points={'3dsMax': 'startup=reloadmod:startup'},
14 | python_requires='>=3.7'
15 | )
16 |
--------------------------------------------------------------------------------
/src/packages/transformlock/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | with open("README.md", "r") as fh:
4 | long_description = fh.read()
5 |
6 | setuptools.setup(
7 | name="transformlock-autodesk",
8 | version="0.0.1",
9 | description="A sample 3ds Max Python Package",
10 | long_description=long_description,
11 | long_description_content_type="text/markdown",
12 | packages=setuptools.find_packages(),
13 | entry_points={'3dsMax': 'startup=transformlock:startup'},
14 | python_requires='>=3.7'
15 | )
16 |
--------------------------------------------------------------------------------
/src/packages/removeallmaterials/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | with open("README.md", "r") as fh:
4 | long_description = fh.read()
5 |
6 | setuptools.setup(
7 | name="removeallmaterials-autodesk",
8 | version="0.0.1",
9 | description="A sample 3ds Max Python Package",
10 | long_description=long_description,
11 | long_description_content_type="text/markdown",
12 | packages=setuptools.find_packages(),
13 | entry_points={'3dsMax': 'startup=removeallmaterials:startup'},
14 | python_requires='>=3.7'
15 | )
16 |
--------------------------------------------------------------------------------
/src/packages/mxthread/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | with open("README.md", "r") as fh:
4 | long_description = fh.read()
5 |
6 | setuptools.setup(
7 | name="mxthread-autodesk",
8 | version="0.0.1",
9 | description="mxthread sample",
10 | long_description=long_description,
11 | long_description_content_type="text/markdown",
12 | url="https://git.autodesk.com/windish/maxpythontutorials",
13 | packages=setuptools.find_packages(),
14 | install_requires=[
15 | 'qtpy'
16 | ],
17 | python_requires='>=3.7'
18 | )
19 |
--------------------------------------------------------------------------------
/src/packages/mxs2py/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | with open("README.md", "r") as fh:
4 | long_description = fh.read()
5 |
6 | setuptools.setup(
7 | name="mxs2py-autodesk",
8 | version="0.0.1",
9 | description="mxs2py converter",
10 | long_description=long_description,
11 | long_description_content_type="text/markdown",
12 | url="https://github.com/ADN-DevTech/3dsMax-Python-HowTos",
13 | packages=setuptools.find_packages(),
14 | install_requires = [
15 | 'parsec==3.12'
16 | ],
17 | python_requires='>=3.7'
18 | )
19 |
--------------------------------------------------------------------------------
/.github/workflows/checks-workflow.yml:
--------------------------------------------------------------------------------
1 | name: Checks
2 | on: [push, pull_request]
3 | jobs:
4 | build:
5 | runs-on: ubuntu-latest
6 | steps:
7 | # This step checks out a copy of your repository.
8 | - uses: actions/checkout@v2
9 | # This step references the directory that contains the action.
10 | - name: RunChecks
11 | # this is a mega hack, to be fixed
12 | run: |
13 | sudo apt-get install dos2unix
14 | python -m pip install pylint
15 | chmod +x ./scripts/checks.sh
16 | ./scripts/checks.sh
17 |
--------------------------------------------------------------------------------
/src/packages/mxvscode/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | with open("README.md", "r") as fh:
4 | long_description = fh.read()
5 |
6 | setuptools.setup(
7 | name="mxvscode-autodesk",
8 | version="0.0.1",
9 | description="Let the VS Code debugger attach to 3ds Max",
10 | long_description=long_description,
11 | long_description_content_type="text/markdown",
12 | packages=setuptools.find_packages(),
13 | install_requires=[
14 | 'debugpy'
15 | ],
16 | entry_points={'3dsMax': 'startup=mxvscode:startup'},
17 | python_requires='>=3.7'
18 | )
19 |
--------------------------------------------------------------------------------
/src/packages/renameselected/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | with open("README.md", "r") as fh:
4 | long_description = fh.read()
5 |
6 | setuptools.setup(
7 | name="renameselected-autodesk",
8 | version="0.0.1",
9 | description="A sample 3ds Max Python Package",
10 | long_description=long_description,
11 | long_description_content_type="text/markdown",
12 | packages=setuptools.find_packages(),
13 | entry_points={'3dsMax': 'startup=renameselected:startup'},
14 | install_requires=[
15 | 'qtpy'
16 | ],
17 | python_requires='>=3.7'
18 | )
19 |
--------------------------------------------------------------------------------
/src/samples/pymxs/enumerate_parameters.py:
--------------------------------------------------------------------------------
1 | '''
2 | Creates all geometric objects and lists their parameters.
3 | '''
4 | from pymxs import runtime as rt # pylint: disable=import-error
5 |
6 | for cl in rt.GeometryClass.classes:
7 | try:
8 | obj = cl()
9 | print(f"Properties for class {cl}")
10 | # This would also work:
11 | # rt.showProperties(o)
12 | for pn in rt.getPropNames(obj):
13 | print(f" {pn} {rt.getProperty(obj, pn)}")
14 | except RuntimeError:
15 | # Some geometry classes cannot be instantiated
16 | pass
17 |
--------------------------------------------------------------------------------
/src/packages/singleinstancedlg/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | with open("README.md", "r") as fh:
4 | long_description = fh.read()
5 |
6 | setuptools.setup(
7 | name="singleinstancedlg-autodesk",
8 | version="0.0.1",
9 | description="Single instance modeless dialog",
10 | long_description=long_description,
11 | long_description_content_type="text/markdown",
12 | packages=setuptools.find_packages(),
13 | entry_points={'3dsMax': 'startup=singleinstancedlg:startup'},
14 | install_requires=[
15 | 'qtpy'
16 | ],
17 | python_requires='>=3.7'
18 | )
19 |
--------------------------------------------------------------------------------
/src/packages/threadprogressbar/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | with open("README.md", "r") as fh:
4 | long_description = fh.read()
5 |
6 | setuptools.setup(
7 | name="threadprogressbar-autodesk",
8 | version="0.0.1",
9 | description="Update a progress bar from a thread",
10 | long_description=long_description,
11 | long_description_content_type="text/markdown",
12 | packages=setuptools.find_packages(),
13 | entry_points={'3dsMax': 'startup=threadprogressbar:startup'},
14 | install_requires=[
15 | 'qtpy'
16 | ],
17 | python_requires='>=3.7'
18 | )
19 |
--------------------------------------------------------------------------------
/src/packages/socketioclient/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | with open("README.md", "r") as fh:
4 | long_description = fh.read()
5 |
6 | setuptools.setup(
7 | name="socketioclient-autodesk",
8 | version="0.0.1",
9 | description="socketioclient sample",
10 | long_description=long_description,
11 | long_description_content_type="text/markdown",
12 | url="https://git.autodesk.com/windish/maxpythontutorials",
13 | packages=setuptools.find_packages(),
14 | install_requires=[
15 | 'python-socketio',
16 | 'websocket-client'
17 | ],
18 | python_requires='>=3.7'
19 | )
20 |
--------------------------------------------------------------------------------
/src/samples/pymxs/output_plugin_classes.py:
--------------------------------------------------------------------------------
1 | '''
2 | Demonstrates using the PluginManager to extract information about loaded
3 | plugins.
4 | '''
5 | from pymxs import runtime as rt # pylint: disable=import-error
6 |
7 | # List all plug-in dlls
8 | PLUGIN_COUNT = rt.pluginManager.pluginDllCount
9 | print(f"Total PluginDlls: {PLUGIN_COUNT}\n")
10 | # maxscript uses one based indices
11 | for p in range(1, PLUGIN_COUNT + 1):
12 | print("PluginDll:", rt.pluginManager.pluginDllFullPath(p))
13 | print("Description:", rt.pluginManager.pluginDllName(p))
14 | print("Loaded:", rt.pluginManager.isPluginDllLoaded(p))
15 |
--------------------------------------------------------------------------------
/src/samples/pymxs/hit_test.py:
--------------------------------------------------------------------------------
1 | '''
2 | Performs a hit test on an object in the active viewport.
3 | '''
4 | from pymxs import runtime as rt # pylint: disable=import-error
5 |
6 | def main():
7 | """Demonstrate hit testing of rays with scene objects."""
8 | obj = rt.sphere(radius=50)
9 | point = rt.Point2(400, 200)
10 | hit_ray = rt.intersectRay(obj, rt.mapScreenToWorldRay(point))
11 | print(f"hit success {bool(hit_ray)} for point {point}")
12 | point = rt.Point2(0, 0)
13 | hit_ray = rt.intersectRay(obj, rt.mapScreenToWorldRay(point))
14 | print(f"hit success {bool(hit_ray)} for point {point}")
15 |
16 | main()
17 |
--------------------------------------------------------------------------------
/src/samples/pymxs/get_rendered_normals.py:
--------------------------------------------------------------------------------
1 | '''
2 | An example of how to get the vertices and normals of a node
3 | '''
4 | from pymxs import runtime as rt # pylint: disable=import-error
5 |
6 | def main():
7 | """Create a box and display its vertices and normals."""
8 | box = rt.box()
9 | print(f"node name: {box.name}")
10 | trimesh = rt.convertTo(box, rt.TrimeshGeometry)
11 | print(f" verts: {trimesh.numVerts}")
12 | for vert in range(trimesh.numVerts):
13 | normal = rt.getNormal(trimesh, vert + 1)
14 | vertex = rt.getVert(trimesh, vert + 1)
15 | print(f"vertex: {vertex}")
16 | print(f"RNormal: {normal}")
17 | main()
18 |
--------------------------------------------------------------------------------
/src/packages/mxstranslate/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | with open("README.md", "r") as fh:
4 | long_description = fh.read()
5 |
6 | setuptools.setup(
7 | name="mxstranslate-autodesk",
8 | version="0.0.1",
9 | description="Translation window for mxs code",
10 | long_description=long_description,
11 | long_description_content_type="text/markdown",
12 | url="https://git.autodesk.com/windish/maxpythontutorials",
13 | packages=setuptools.find_packages(),
14 | entry_points={'3dsMax': 'startup=mxstranslate:startup'},
15 | install_requires=[
16 | 'pygments',
17 | 'qtpy'
18 | ],
19 | python_requires='>=3.7'
20 | )
21 |
--------------------------------------------------------------------------------
/src/packages/pyconsole/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | with open("README.md", "r") as fh:
4 | long_description = fh.read()
5 |
6 | setuptools.setup(
7 | name="pyconsole-autodesk",
8 | version="0.0.1",
9 | description="pyconsole sample",
10 | long_description=long_description,
11 | long_description_content_type="text/markdown",
12 | url="https://git.autodesk.com/windish/maxpythontutorials",
13 | packages=setuptools.find_packages(),
14 | install_requires=[
15 | 'jedi==0.19.1',
16 | 'pyqtconsole',
17 | 'qtpy'
18 | ],
19 | entry_points={'3dsMax': 'startup=pyconsole:startup'},
20 | python_requires='>=3.7'
21 | )
22 |
--------------------------------------------------------------------------------
/uninstall.sh:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env bash
2 | set -e
3 | script="$(dirname "$(readlink -f "$0")")"
4 | source "$script/scripts/inst.sh"
5 |
6 | # make sure we have 3ds Max in the current path
7 | if [ ! -f ./3dsmax.exe ]
8 | then
9 | exiterr "This script must run in a 3ds Max installation directory."
10 | fi
11 |
12 | echo "Uninstall python packages"
13 | "$script"/uninstallhowtos.sh
14 |
15 | echo "Uninstall pip"
16 | (
17 | cd "$pythonpath"
18 | ./python.exe -m pip uninstall pip
19 | )
20 |
21 | echo "Uninstall pystartup and adn-devtech-python-howtos"
22 | rm -f "$startuppath/pystartup.ms"
23 | rm -fr "$ProgramData/Autodesk/ApplicationPlugins/adn-devtech-python-howtos"
24 |
25 |
--------------------------------------------------------------------------------
/src/packages/reloadmod/reloadmod/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Add a menu item for reloading all development modules.
3 | """
4 | import menuhook
5 | from pymxs import runtime as rt
6 | from reloadmod import reload
7 |
8 | def python_reload():
9 | """
10 | Reload all development modules.
11 | """
12 | reload.reload_many(reload.dev_only())
13 |
14 | def startup():
15 | """
16 | Hook the function to a menu item.
17 | """
18 | menuhook.register(
19 | "reloadmod",
20 | "python3devtools",
21 | python_reload,
22 | menu=["&Scripting", "Python3 Development"],
23 | text="Reload Python Modules",
24 | tooltip="Reload Python Modules",
25 | in2025_menuid=menuhook.PYTHON_DEVELOPMENT,
26 | id_2025="7A8CFC05-0752-4501-A5BD-F5AF020D7F5F")
27 |
--------------------------------------------------------------------------------
/src/packages/singleinstancedlg/singleinstancedlg/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | singleinstancedlg example: Single instance modeless dialog
3 | """
4 | import menuhook
5 | from pymxs import runtime as rt
6 | from singleinstancedlg import ui
7 |
8 | def singleinstancedlg():
9 | '''Show a dialog if not already there'''
10 | ui.show_dialog()
11 |
12 | def startup():
13 | """
14 | Hook the function to a menu item.
15 | """
16 | menuhook.register(
17 | "singleinstancedlg",
18 | "howtos",
19 | singleinstancedlg,
20 | menu=["&Scripting", "Python3 Development", "Other Samples"],
21 | text="Single instance modeless dialog",
22 | tooltip="Single instance modeless dialog",
23 | in2025_menuid=menuhook.OTHER_SAMPLES,
24 | id_2025="AF515BBA-E826-4DA8-B097-FA9A2C917A91")
25 |
--------------------------------------------------------------------------------
/src/packages/transformlock/transformlock/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | transformlock example: lock all transforms on the selection.
3 | """
4 | import menuhook
5 | from pymxs import runtime as rt
6 |
7 | def lock_selection():
8 | '''Lock all transforms on the selection'''
9 | rt.setTransformLockFlags(rt.selection, rt.Name("all"))
10 |
11 | def startup():
12 | """
13 | Hook the transform lock function to a menu item.
14 | """
15 | menuhook.register(
16 | "transformlock",
17 | "howtos",
18 | lock_selection,
19 | menu=["&Scripting", "Python3 Development", "How To"],
20 | text="Lock transformations for the selection",
21 | tooltip="Lock transformations for the selection",
22 | in2025_menuid=menuhook.HOW_TO,
23 | id_2025="F9DA574D-185B-4C00-9C37-795B38719E78")
24 |
--------------------------------------------------------------------------------
/src/samples/pymxs/sphere_borg.py:
--------------------------------------------------------------------------------
1 | '''
2 | Demonstrates creating objects, object instancing, and object translation.
3 | '''
4 | from pymxs import runtime as rt # pylint: disable=import-error
5 |
6 | INST = rt.Name("instance")
7 |
8 | def create_borg(obj, num, spacing):
9 | """Create a bunch of clones of the provided object"""
10 | for i in range(num):
11 | for j in range(num):
12 | for k in range(num):
13 | if i or j or k:
14 | point = rt.Point3(i * spacing, j * spacing, k * spacing)
15 | rt.MaxOps.CloneNodes(obj, cloneType=INST, offset=point)
16 |
17 | def main():
18 | """Create a base object and turn it into a borg, whatever that is."""
19 | obj = rt.sphere()
20 | obj.Radius = 2.0
21 | create_borg(obj, 4, 5.0)
22 |
23 | main()
24 |
--------------------------------------------------------------------------------
/src/packages/removeallmaterials/removeallmaterials/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | removeallmaterials example: remove all materials from the scene
3 | """
4 | import menuhook
5 | from pymxs import runtime as rt
6 |
7 | def remove_all_materials():
8 | '''Remove all materials from the scene'''
9 | for obj in rt.objects:
10 | obj.material = None
11 |
12 | def startup():
13 | """
14 | Hook the function to a menu item.
15 | """
16 | menuhook.register(
17 | "removeallmaterials",
18 | "howtos",
19 | remove_all_materials,
20 | menu=["&Scripting", "Python3 Development", "How To"],
21 | text="Remove all materials from the scene",
22 | tooltip="Remove all materials from the scene",
23 | in2025_menuid=menuhook.HOW_TO,
24 | id_2025="1A3AE016-3E54-4856-9076-2BE491B2258C")
25 |
--------------------------------------------------------------------------------
/src/packages/mxstranslate/mxstranslate/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | mxstranslate example: Translation window for mxs code
3 | """
4 | import menuhook
5 | from pymxs import runtime as rt
6 | from mxstranslate import translate
7 |
8 | def mxstranslate():
9 | '''Translation window for mxs code'''
10 | # Create a console and float it
11 | translate.new_editor(floating=True)
12 |
13 | def startup():
14 | """
15 | Hook the function to a menu item.
16 | """
17 | menuhook.register(
18 | "mxstranslate",
19 | "howtos",
20 | mxstranslate,
21 | menu=["&Scripting", "Python3 Development", "How To"],
22 | text="Translation window for mxs code",
23 | tooltip="Translation window for mxs code",
24 | in2025_menuid=menuhook.HOW_TO,
25 | id_2025="004BF4E1-4AB3-42B5-979A-28662B26533C")
26 |
--------------------------------------------------------------------------------
/src/samples/pymxs/tree_of_spheres.py:
--------------------------------------------------------------------------------
1 | '''
2 | Creates a hierarchy of sphere objects at different relative locations.
3 | '''
4 | from pymxs import runtime as rt # pylint: disable=import-error
5 |
6 | def create_sphere():
7 | """Create and return a single sphere of radius 5."""
8 | sphere = rt.sphere()
9 | sphere.radius = 5
10 | return sphere
11 |
12 | def tree_of_spheres(parent, width, xinc, depth, maxdepth):
13 | """Create a tree of spheres."""
14 | if depth == maxdepth:
15 | return
16 | for i in range(width):
17 | sphere = create_sphere()
18 | pos = parent.pos
19 | sphere.pos = rt.Point3(pos.x + i * xinc, 0, pos.z + 15)
20 | sphere.Parent = parent
21 | tree_of_spheres(sphere, width, xinc * width, depth + 1, maxdepth)
22 |
23 | tree_of_spheres(create_sphere(), 2, 10, 0, 4)
24 |
--------------------------------------------------------------------------------
/src/packages/pyconsole/pyconsole/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | pyconsole example: pyconsole sample
3 | """
4 | import menuhook
5 | from pyconsole import console
6 |
7 | def pyconsole():
8 | '''pyconsole sample'''
9 | # Create a console and float it
10 | console.new_console(floating=True)
11 |
12 | def startup():
13 | """
14 | Hook the function to a menu item.
15 | """
16 | menuhook.register(
17 | "pyconsole",
18 | "howtos",
19 | pyconsole,
20 | menu=["&Scripting", "Python3 Development", "How To"],
21 | text="Python Console",
22 | tooltip="Python Console",
23 | in2025_menuid=menuhook.HOW_TO,
24 | id_2025="0F52AF28-D7EE-4A04-AC9D-56C126FE9373")
25 | # Create a python console in the command panel
26 | # automatically
27 | console.new_console(tabto="CommandPanel")
28 |
--------------------------------------------------------------------------------
/src/packages/threadprogressbar/threadprogressbar/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | threadprogressbar example: Update a progress bar from a thread
3 | """
4 | import menuhook
5 | from pymxs import runtime as rt
6 | from threadprogressbar import ui
7 |
8 | def threadprogressbar():
9 | '''Update a progress bar from a thread'''
10 | dialog = ui.PyMaxDialog()
11 | dialog.show()
12 |
13 | def startup():
14 | """
15 | Hook the function to a menu item.
16 | """
17 | menuhook.register(
18 | "threadprogressbar",
19 | "howtos",
20 | threadprogressbar,
21 | menu=["&Scripting", "Python3 Development", "Other Samples"],
22 | text="Update a progress bar from a thread",
23 | tooltip="Update a progress bar from a thread",
24 | in2025_menuid=menuhook.OTHER_SAMPLES,
25 | id_2025="AB072ECE-8665-4EC4-8D12-C0E76DA4C919")
26 |
--------------------------------------------------------------------------------
/src/pystartup/pystartup.ms:
--------------------------------------------------------------------------------
1 | if isProperty python "execute" and ((maxversion())[8]<2025) then (
2 | python.execute ("def _python_startup():\n" +
3 | " try:\n" +
4 | " import pkg_resources\n" +
5 | " except ImportError:\n" +
6 | " print('startup Python modules require pip to be installed.')\n" +
7 | " return\n" +
8 | " for dist in pkg_resources.working_set: \n" +
9 | " entrypt = pkg_resources.get_entry_info(dist, '3dsMax', 'startup')\n" +
10 | " if not (entrypt is None):\n" +
11 | " try:\n" +
12 | " fcn = entrypt.load()\n" +
13 | " fcn()\n" +
14 | " except Exception as e:\n" +
15 | " print(f'skipped package startup for {dist} because {e}, startup not working')\n" +
16 | "_python_startup()\n" +
17 | "del _python_startup")
18 | )
19 |
--------------------------------------------------------------------------------
/src/packages/mxvscode/mxvscode/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Enable vscode debugging during the startup of 3ds Max.
3 | """
4 | import sys
5 | import os
6 | import debugpy
7 |
8 | def startup():
9 | """
10 | Allow the remote vscode debugger to attach to the 3ds Max Python
11 | interpreter
12 | """
13 | print("""mxvscode startup enabling vscode debugging
14 | (if you don't use VSCode for debugging Python you can uninstall
15 | mxvscode)""")
16 |
17 | sysexec = sys.executable
18 | (base, file) = os.path.split(sys.executable)
19 | if file.lower() == "3dsmax.exe":
20 | sys.executable = os.path.join(base, "python", "python.exe")
21 | host = "localhost"
22 | port = 5678
23 | debugpy.listen((host, port))
24 | print(f"-- now ready to receive debugging connections from vscode on (${host}, ${port})")
25 | sys.executable = sysexec
26 |
--------------------------------------------------------------------------------
/src/adn-devtech-python-howtos/PackageContents.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/packages/renameselected/renameselected/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | renameselected example
3 | """
4 | import menuhook
5 | from pymxs import runtime as rt
6 | from renameselected import ui
7 |
8 | def renameselected(text):
9 | '''Rename all elements in selection'''
10 | if text != "":
11 | for i in rt.selection:
12 | i.name = rt.uniquename(text)
13 |
14 | def showdialog():
15 | """
16 | Show the rename dialog.
17 | """
18 | dialog = ui.PyMaxDialog(renameselected)
19 | dialog.show()
20 |
21 | def startup():
22 | """
23 | Hook the function to a menu item.
24 | """
25 | menuhook.register(
26 | "renameselected",
27 | "howtos",
28 | showdialog,
29 | menu=["&Scripting", "Python3 Development", "How To"],
30 | text="Rename all elements in selection",
31 | tooltip="renameselected sample",
32 | in2025_menuid=menuhook.HOW_TO,
33 | id_2025="163ACF54-313D-4B1B-8615-F6F979AE0FE7")
34 |
--------------------------------------------------------------------------------
/src/samples/pymxs/combine_selected_meshes.py:
--------------------------------------------------------------------------------
1 | '''
2 | Demonstrates combining multipe scene nodes to an editable mesh.
3 | '''
4 | from pymxs import runtime as rt # pylint: disable=import-error
5 |
6 | def combine_objects(*args):
7 | """
8 | Convert the two provided objects to meshes and merges them as a single mesh.
9 | """
10 | first = rt.convertToMesh(args[0])
11 | for obj in args[1:]:
12 | rt.attach(first, rt.convertToMesh(obj))
13 |
14 | return first
15 |
16 | def combine_selected_objects():
17 | """
18 | Convert the selected objects into an editable mesh. The selections needs to contain
19 | at least 2 objects to combine.
20 | """
21 | if len(rt.selection) < 2:
22 | msg = "Please select at least 2 nodes to combine."
23 | print(msg)
24 | rt.messageBox(msg)
25 | else:
26 | # combine all the selected nodes into one editable mesh
27 | combine_objects(*rt.selection)
28 |
29 | combine_selected_objects()
30 |
--------------------------------------------------------------------------------
/src/samples/pymxs/apply_material.py:
--------------------------------------------------------------------------------
1 | '''
2 | Applies a standard material to all nodes in the scene.
3 | Also shows the use of generator functions in Python.
4 | '''
5 | from pymxs import runtime as rt # pylint: disable=import-error
6 |
7 | def create_sphere():
8 | """Create a sphere of radius 5."""
9 | return rt.sphere(radius=5)
10 |
11 | def solid_material(color):
12 | """Create a material."""
13 | material = rt.StandardMaterial()
14 | material.Ambient = color
15 | material.Diffuse = color
16 | material.Specular = rt.Color(255, 255, 255)
17 | material.Shininess = 50.0
18 | material.ShinyStrength = 70.0
19 | material.SpecularLevel = 70.0
20 | return material
21 |
22 | def apply_material_to_nodes(material, nodes=rt.rootnode.children):
23 | """Apply a material to multiple nodes."""
24 | for node in nodes:
25 | node.Material = material
26 |
27 | create_sphere()
28 | MAT = solid_material(rt.Color(0, 0, 255))
29 | apply_material_to_nodes(MAT)
30 |
--------------------------------------------------------------------------------
/src/packages/renameselected/renameselected/ui.py:
--------------------------------------------------------------------------------
1 | """
2 | Provide a qtpy dialog for the tool.
3 | """
4 | #pylint: disable=no-name-in-module
5 | #pylint: disable=too-few-public-methods
6 | from qtpy.QtWidgets import QWidget, QDialog, QLabel, QLineEdit, QVBoxLayout, QPushButton
7 | from pymxs import runtime as rt
8 |
9 | class PyMaxDialog(QDialog):
10 | """
11 | Custom dialog attached to the 3ds Max main window
12 | """
13 | def __init__(self, click, parent=QWidget.find(rt.windows.getMAXHWND())):
14 | super().__init__(parent)
15 | self.setWindowTitle('Rename')
16 |
17 | main_layout = QVBoxLayout()
18 | label = QLabel("Enter new base name")
19 | main_layout.addWidget(label)
20 |
21 | edit = QLineEdit()
22 | main_layout.addWidget(edit)
23 |
24 | btn = QPushButton("Rename selected objects")
25 | btn.clicked.connect(lambda: click(edit.text()))
26 | main_layout.addWidget(btn)
27 |
28 | self.setLayout(main_layout)
29 | self.resize(250, 100)
30 |
--------------------------------------------------------------------------------
/src/packages/menuhook/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2020 Autodesk, Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/src/packages/mxvscode/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2020 Autodesk, Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/src/packages/reloadmod/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2020 Autodesk, Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/src/packages/speedsheet/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2020 Autodesk, Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/src/packages/inbrowserhelp/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2020 Autodesk, Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/src/packages/quickpreview/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2020 Autodesk, Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/src/packages/renameselected/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2020 Autodesk, Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/src/packages/transformlock/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2020 Autodesk, Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/src/packages/zdepthchannel/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2020 Autodesk, Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/src/packages/removeallmaterials/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2020 Autodesk, Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/src/packages/singleinstancedlg/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2020 Autodesk, Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/src/packages/threadprogressbar/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2020 Autodesk, Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/src/samples/PySide/ui_loader.py:
--------------------------------------------------------------------------------
1 | '''
2 | Demonstrates loading .ui files with PySide
3 | '''
4 | import os
5 | from qtpy.QtWidgets import QMainWindow
6 | from qtpy.QtCore import QFile
7 | from qtpy.QtUiTools import QUiLoader
8 | from pymxs import runtime as rt
9 | from qtmax import GetQMaxMainWindow
10 |
11 | class MyWindow(QMainWindow):
12 | """
13 | Main window class object loading a .ui file
14 | """
15 | def __init__(self, parent=None):
16 | super(MyWindow, self).__init__(parent)
17 | self.setWindowTitle('Pyside2 Qt Window')
18 | self.init_ui()
19 |
20 | def init_ui(self):
21 | """ Prepare Qt UI layout for main window content """
22 | ui_file = QFile(os.path.dirname(os.path.realpath(__file__)) + "\\test_ui.ui")
23 | ui_file.open(QFile.ReadOnly)
24 | self.loaded_ui = QUiLoader().load(ui_file, self)
25 | ui_file.close()
26 |
27 | def demo_ui_loader():
28 | """
29 | Entry point to demonstrate how to load a .ui file
30 | """
31 | win = MyWindow(GetQMaxMainWindow())
32 | win.show()
33 |
34 | demo_ui_loader()
35 |
--------------------------------------------------------------------------------
/src/adn-devtech-python-howtos/scripts/pyStartup.py:
--------------------------------------------------------------------------------
1 | def _python_startup():
2 | try:
3 | import pkg_resources
4 | except ImportError:
5 | print('startup Python modules require pip to be installed.')
6 | return
7 | for dist in pkg_resources.working_set:
8 | entrypt = pkg_resources.get_entry_info(dist, '3dsMax', 'startup')
9 | if not (entrypt is None):
10 | try:
11 | fcn = entrypt.load()
12 | fcn()
13 | except Exception as e:
14 | print(f'skipped package startup for {dist} because {e}, startup not working')
15 |
16 | # configure 2025 menus
17 | from pymxs import runtime as rt
18 | from menuhook import register_howtos_menu_2025
19 | def menu_func():
20 | menumgr = rt.callbacks.notificationparam()
21 | register_howtos_menu_2025(menumgr)
22 |
23 | # menu system
24 | cuiregid = rt.name("cuiRegisterMenus")
25 | howtoid = rt.name("pyScriptHowtoMenu")
26 | rt.callbacks.removescripts(id=cuiregid)
27 | rt.callbacks.addscript(cuiregid, menu_func, id=howtoid)
28 |
29 |
30 | _python_startup()
31 |
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Autodesk Developer Network
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/packages/mxs2py/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Autodesk Developer Network
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/packages/quickpreview/quickpreview/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | quickpreview example: Create a quick preview
3 | """
4 | from os import path
5 | import menuhook
6 | from pymxs import runtime as rt
7 |
8 | def quickpreview():
9 | '''Create a quick preview'''
10 | preview_name = path.join(rt.getDir(rt.Name("preview")), "quickpreview.avi")
11 | view_size = rt.getViewSize()
12 | anim_bmp = rt.bitmap(view_size.x, view_size.y, filename=preview_name)
13 | for t in range(int(rt.animationRange.start), int(rt.animationRange.end)):
14 | rt.sliderTime = t
15 | dib = rt.gw.getViewportDib()
16 | rt.copy(dib, anim_bmp)
17 | rt.save(anim_bmp)
18 | rt.close(anim_bmp)
19 | rt.gc()
20 | rt.ramplayer(preview_name, "")
21 |
22 | def startup():
23 | """
24 | Hook the function to a menu item.
25 | """
26 | menuhook.register(
27 | "quickpreview",
28 | "howtos",
29 | quickpreview,
30 | menu=["&Scripting", "Python3 Development", "How To"],
31 | text="Create a quick preview",
32 | tooltip="Create a quick preview",
33 | in2025_menuid=menuhook.HOW_TO,
34 | id_2025="E30C825C-E4CD-48FD-A1C7-A75A91B2E9C2")
35 |
--------------------------------------------------------------------------------
/uninstallhowtos.sh:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env bash
2 | set -e
3 | script="$(dirname "$(readlink -f "$0")")"
4 | source "$script/scripts/inst.sh"
5 |
6 | venvscript () {
7 | echo "cd Scripts"
8 | echo "call activate.bat"
9 | for f in $(find "$packagedir" -name "setup.py")
10 | do
11 | local package="$(basename "$(dirname "$f")")"
12 | echo "pip.exe uninstall -y $package-autodesk"
13 | done
14 | }
15 |
16 | if [ -f "Scripts/activate.bat" ]
17 | then
18 | # this is a virtual env
19 | echo "unininstall python packages from virtual env"
20 | read -p "Is this what you want (Y/n)? " -r
21 | if [[ $REPLY =~ ^[Yy]$ ]]
22 | then
23 | tmpfile="$(mktemp /tmp/XXXXXXX.bat)"
24 | venvscript > "$tmpfile"
25 | cmd //C "$tmpfile"
26 | rm "$tmpfile"
27 | fi
28 | elif [ -f ./3dsmax.exe ]
29 | then
30 | # this is the default installation
31 | echo "uininstall python packages from your user account"
32 | read -p "Is this what you want (Y/n)? " -r
33 | if [[ $REPLY =~ ^[Yy]$ ]]
34 | then
35 | uninstallpythonpackages
36 | fi
37 | else
38 | exiterr "This script must run in a 3ds Max installation directory or in a virtual environment directory."
39 | fi
40 |
41 |
--------------------------------------------------------------------------------
/src/packages/singleinstancedlg/singleinstancedlg/ui.py:
--------------------------------------------------------------------------------
1 | """
2 | qtpy modeless dialog that will not be started more than once
3 | at the same time
4 | """
5 | #pylint: disable=no-name-in-module
6 | #pylint: disable=too-few-public-methods
7 | from qtpy.QtWidgets import QWidget, QDialog, QVBoxLayout, QPushButton
8 | from pymxs import runtime as rt
9 |
10 | MAIN_WINDOW = QWidget.find(rt.windows.getMAXHWND())
11 |
12 | class PyMaxDialog(QDialog):
13 | """
14 | Custom dialog attached to the 3ds Max main window
15 | """
16 | unique_name = __file__
17 | def __init__(self, parent):
18 | super().__init__(parent)
19 | self.setWindowTitle('Single Instance Dialog')
20 |
21 | # keep track of being unique
22 | self.setObjectName(PyMaxDialog.unique_name)
23 |
24 | main_layout = QVBoxLayout()
25 |
26 | btn = QPushButton("Dummy Button")
27 | main_layout.addWidget(btn)
28 |
29 | self.setLayout(main_layout)
30 | self.resize(250, 100)
31 |
32 | def show_dialog():
33 | '''Show the dialog without duplicating it'''
34 | dialog = MAIN_WINDOW.findChild(QDialog, PyMaxDialog.unique_name)
35 | if dialog is None:
36 | dialog = PyMaxDialog(MAIN_WINDOW)
37 | dialog.show()
38 |
--------------------------------------------------------------------------------
/src/samples/pymxs/poly_object.py:
--------------------------------------------------------------------------------
1 | '''
2 | Demonstrates how to create a mmesh from scratch and to set color per vertex data.
3 | '''
4 | from pymxs import runtime as rt # pylint: disable=import-error
5 |
6 | def make_pyramid_mesh(side=20.0):
7 | '''Construct a pyramid from vertices and faces.'''
8 | halfside = side / 2.0
9 | return rt.mesh(
10 | vertices=[
11 | rt.point3(0.0, 0.0, side),
12 | rt.point3(-halfside, -halfside, 0.0),
13 | rt.point3(-halfside, halfside, 0.0),
14 | rt.point3(halfside, 0.0, 0.0)
15 | ],
16 | faces=[
17 | rt.point3(1, 2, 3),
18 | rt.point3(1, 3, 4),
19 | rt.point3(1, 4, 2),
20 | rt.point3(2, 3, 4),
21 | ])
22 |
23 | def color_pyramid_mesh(mesh):
24 | '''Add two color vertices, and refer them in the faces (color the pyramid).'''
25 | rt.setNumCPVVerts(mesh, 2, True)
26 | rt.setVertColor(mesh, 1, rt.Point3(255, 0, 0))
27 | rt.setVertColor(mesh, 2, rt.Point3(0, 0, 255))
28 | rt.buildVCFaces(mesh)
29 | rt.setVCFace(mesh, 1, 1, 1, 2)
30 | rt.setVCFace(mesh, 2, 1, 2, 2)
31 | rt.setVCFace(mesh, 3, 2, 2, 2)
32 | rt.setVCFace(mesh, 4, 1, 1, 1)
33 | rt.setCVertMode(mesh, True)
34 | rt.update(mesh)
35 |
36 | def main():
37 | '''Construct a pyramid and then add colors to its faces.'''
38 | rt.resetMaxFile(rt.Name('noPrompt'))
39 | mesh = make_pyramid_mesh()
40 | color_pyramid_mesh(mesh)
41 |
42 | main()
43 |
--------------------------------------------------------------------------------
/src/samples/pymxs/make_instances.py:
--------------------------------------------------------------------------------
1 | '''
2 | Demonstrates creating instances of a node hierarchy.
3 | '''
4 | import pymxs # pylint: disable=import-error
5 | from pymxs import runtime as rt # pylint: disable=import-error
6 |
7 | INST = rt.Name("instance")
8 |
9 | def create_instance_clones(obj, count, offset):
10 | """Create count clones of obj setting the parent of each clone to the
11 | previous clone."""
12 | for _ in range(count):
13 | # the maxscript CloneNodes method accepts a named argument called 'newNodes'
14 | # the argument must be sent by reference as it serves as an output argument
15 | # since the argument is not also an input argument, we can simply initialize
16 | # the byref() object as 'None'
17 | # the output argument along with the call result is then returned in a tuple
18 | # note: 'newNodes' returns an array of cloned nodes
19 | # in the current case, only one element is cloned
20 | result, cloned = rt.MaxOps.CloneNodes(obj, cloneType=INST, offset=offset, newNodes=pymxs.byref(None))
21 | cloned[0].parent = obj
22 | obj = cloned[0]
23 |
24 | def main():
25 | """Demonstrate cloning"""
26 | rt.resetMaxFile(rt.Name('noPrompt'))
27 |
28 | obj = rt.sphere(radius=3)
29 | create_instance_clones(obj, 10, rt.Point3(5, 0, 0))
30 | rt.MaxOps.CloneNodes(
31 | obj,
32 | cloneType=INST,
33 | offset=rt.Point3(0, 25, 0),
34 | expandHierarchy=True)
35 |
36 | main()
37 |
--------------------------------------------------------------------------------
/src/samples/pymxs/pymxs_classes.py:
--------------------------------------------------------------------------------
1 | '''
2 | Demonstrates using the inspect module to list all of the classes in
3 | pymxs and the total number of members exposed.
4 |
5 | (Note that there is no 1:1 relationship between the classes in
6 | maxscript and the python classes exposed in pymxs: a single pymxs
7 | wrapper exposes almost all maxscript classes. You can use
8 | pymxs.runtime.apropos(), showClass(), and other MAXScript inspection
9 | methods to get information about MAXScript objects and classes)
10 | '''
11 | import os
12 | import inspect
13 | import pymxs # pylint: disable=import-error
14 | from pymxs import runtime as rt # pylint: disable=import-error
15 |
16 | def inspect_pymxs():
17 | """Inspect the classes exported by pymxs and generate a report."""
18 | api = {}
19 | classes = inspect.getmembers(pymxs, inspect.isclass)
20 | totalcnt = 0
21 | for curclass in classes:
22 | name = str(curclass[0])
23 | membercnt = len(curclass[1].__dict__)
24 | totalcnt += membercnt
25 | api[name] = membercnt
26 |
27 | fname = os.path.join(rt.sysInfo.tempdir, 'pyms_api.txt')
28 | with open(fname, 'w') as output:
29 | for k in sorted(api.keys()):
30 | output.write(k + " has " + str(api[k]) + " members\n")
31 |
32 | print("Results saved to", fname)
33 | print("Total number of classes ", len(api))
34 | print("Total number of API elements ", totalcnt)
35 | print("Average number of API elements per class ", totalcnt / len(api))
36 |
37 | inspect_pymxs()
38 |
--------------------------------------------------------------------------------
/src/packages/socketioclient/socketioclient/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Demonstrate the use of of a socket.io client in 3dsMax
3 | """
4 | import threading
5 | import socketio
6 | import pymxs
7 | import mxthread
8 |
9 | def connect_socketio():
10 | """Fonnect the socket io client"""
11 | sio = socketio.Client()
12 |
13 | @sio.event
14 | def connect():
15 | """Handle the connection"""
16 | # note: socketio calls us on a different thread.
17 | # 3dsMax (pymxs, the print function) don't work from a thread.
18 | # We use mxthread (from a different example) to execute code on
19 | # 3dsMax's main thread to handle the event. Note that this will
20 | # deadlock if the main thread is blocked (we cannot execute something
21 | # on the main thread if the main thread is blocked).
22 | mxthread.run_on_main_thread(print, "connection established")
23 |
24 | @sio.on("chat message")
25 | def my_message(data):
26 | """Handle a chat message"""
27 | mxthread.run_on_main_thread(print, f"message received {data}")
28 | # We could emit something here
29 | # sio.emit('my response', {'response': 'my response'})
30 |
31 | # We could disconnect ourself (so we only handle one message)
32 | sio.disconnect()
33 |
34 | @sio.event
35 | def disconnect():
36 | """Handle a disconnection"""
37 | mxthread.run_on_main_thread(print, "disconnected from server")
38 |
39 | sio.connect('http://localhost:3000')
40 | sio.wait()
41 | mxthread.run_on_main_thread(print, "no longer waiting")
42 |
43 | x = threading.Thread(target=connect_socketio)
44 | x.start()
45 |
--------------------------------------------------------------------------------
/installhowtos.sh:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env bash
2 | set -e
3 | script="$(dirname "$(readlink -f "$0")")"
4 | source "$script/scripts/inst.sh"
5 | echo "Installing for 3ds Max version: $version. If this is not correct please set the VERSION environment variable before runnning the script. (ex: VERSION=2022)"
6 |
7 | # make sure cygpath is available
8 | if ! command -v cygpath >/dev/null 2>&1
9 | then
10 | exiterr "cygpath needs to be in the path."
11 | fi
12 |
13 | # make sure cmd is available
14 | if ! command -v cmd >/dev/null 2>&1
15 | then
16 | exiterr "cmd needs to be in the path."
17 | fi
18 |
19 | venvscript () {
20 | echo "cd Scripts"
21 | echo "call activate.bat"
22 | for f in $(find "$packagedir" -name "setup.py")
23 | do
24 | local package="$(dirname "$f")"
25 | echo "pip.exe install -e \"$(cygpath -d "$package")\""
26 | done
27 | }
28 |
29 | set -e
30 | if [ -f "Scripts/activate.bat" ]
31 | then
32 | # this is a virtual env
33 | echo "install python packages in virtual env"
34 | read -p "Is this what you want (Y/n)? " -r
35 | if [[ $REPLY =~ ^[Yy]$ ]]
36 | then
37 | tmpfile="$(mktemp /tmp/XXXXXXX.bat)"
38 | venvscript > "$tmpfile"
39 | cmd //C "$tmpfile"
40 | rm "$tmpfile"
41 | fi
42 | elif [ -f ./3dsmax.exe ]
43 | then
44 | # this is the default installation
45 | echo "install python packages in your account with --user (not in a virtual env)"
46 | read -p "Is this what you want (Y/n)? " -r
47 | if [[ $REPLY =~ ^[Yy]$ ]]
48 | then
49 | installpythonpackages
50 | fi
51 | else
52 | exiterr "This script must run in a 3ds Max installation directory or in a virtual environment directory.."
53 | fi
54 |
--------------------------------------------------------------------------------
/src/samples/PySide/simple_dialog.py:
--------------------------------------------------------------------------------
1 | '''
2 | Demonstrates how to create a QDialog with PySide and attach it to the 3ds Max main window.
3 | '''
4 |
5 | from qtpy.QtWidgets import QDialog, QLabel, QVBoxLayout, QPushButton
6 | from pymxs import runtime as rt
7 | from qtmax import GetQMaxMainWindow
8 |
9 | def create_cylinder():
10 | """
11 | Create a cylinder node with predetermined radius and height values.
12 | """
13 | rt.Cylinder(radius=10, height=30)
14 | # force a viewport update for the node to appear
15 | rt.redrawViews()
16 |
17 | class PyMaxDialog(QDialog):
18 | """
19 | Custom dialog attached to the 3ds Max main window
20 | Message label and action push button to create a cylinder in the 3ds Max scene graph
21 | """
22 | def __init__(self, parent=None):
23 | super(PyMaxDialog, self).__init__(parent)
24 | self.setWindowTitle('Pyside2 Qt Window')
25 | self.init_ui()
26 |
27 | def init_ui(self):
28 | """ Prepare Qt UI layout for custom dialog """
29 | main_layout = QVBoxLayout()
30 | label = QLabel("Click button to create a cylinder in the scene")
31 | main_layout.addWidget(label)
32 |
33 | cylinder_btn = QPushButton("Cylinder")
34 | cylinder_btn.clicked.connect(create_cylinder)
35 | main_layout.addWidget(cylinder_btn)
36 |
37 | self.setLayout(main_layout)
38 | self.resize(250, 100)
39 |
40 | def demo_simple_dialog():
41 | """
42 | Entry point for QDialog demo making use of PySide and pymxs
43 | """
44 | # reset 3ds Max
45 | rt.resetMaxFile(rt.Name('noPrompt'))
46 |
47 | dialog = PyMaxDialog(GetQMaxMainWindow())
48 | dialog.show()
49 |
50 | demo_simple_dialog()
51 |
--------------------------------------------------------------------------------
/src/samples/pymxs/transform_nodes.py:
--------------------------------------------------------------------------------
1 | '''
2 | Creates a number of boxes with random scale, position, and rotation.
3 | '''
4 | from random import random as rnd
5 | import math
6 | from pymxs import runtime as rt # pylint: disable=import-error
7 |
8 | def rnd_angle():
9 | """Return a random angle in radians."""
10 | return -math.pi + (rnd() * 2 * math.pi)
11 |
12 | def rnd_quat():
13 | """Return a random quaternion."""
14 | return rt.Quat(rnd(), rnd(), rnd(), rnd_angle())
15 |
16 | def rnd_dist():
17 | """Return a random distance."""
18 | return rnd() * 100.0 - 50.0
19 |
20 | def rnd_position():
21 | """Return a random position."""
22 | return rt.Point3(rnd_dist(), rnd_dist(), 0)
23 |
24 | def rnd_scale_amount():
25 | """Return a random scaling amount."""
26 | return rnd() * 2.0 + 0.1
27 |
28 | def rnd_scale():
29 | """Return a random (x,y,z) scaling as a Point3."""
30 | return rt.Point3(rnd_scale_amount(), rnd_scale_amount(), rnd_scale_amount())
31 |
32 | def random_transform_nodes(nodes):
33 | """Apply a random transformation (scaling, rotation, position)
34 | to a list of nodes."""
35 | for node in nodes:
36 | node.Scaling = rnd_scale()
37 | node.Rotation = rnd_quat()
38 | node.Position = rnd_position()
39 |
40 | def create_nodes(count):
41 | """Return count nodes."""
42 | def make_box():
43 | """Create a single node, a box of (10, 10, 10)."""
44 | box = rt.box()
45 | box.length = 10.0
46 | box.height = 10.0
47 | box.width = 10.0
48 | return box
49 | return [make_box() for i in range(count)]
50 |
51 | def main():
52 | """Demonstrate the generation and transformation of 25 boxes."""
53 | nodes = create_nodes(25)
54 | random_transform_nodes(nodes)
55 |
56 | main()
57 |
--------------------------------------------------------------------------------
/src/packages/zdepthchannel/zdepthchannel/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | zdepthchannel example: Access the Z-Depth Channel
3 | """
4 | import re
5 | import menuhook
6 | from pymxs import runtime as rt
7 |
8 | def zdepthchannel():
9 | '''Access the Z-Depth Channel'''
10 | prev_renderer = rt.renderers.current
11 | rt.renderers.current = rt.Default_Scanline_Renderer()
12 | voxelbox = re.compile("^VoxelBox")
13 | for tbd in filter(lambda o: voxelbox.match(o.name), list(rt.objects)):
14 | rt.delete(tbd)
15 |
16 | zdepth_name = rt.Name("zdepth")
17 | rbmp = rt.render(outputsize=rt.Point2(32, 32), channels=[zdepth_name], vfb=False)
18 | z_d = rt.getChannelAsMask(rbmp, zdepth_name)
19 | rt.progressStart("Rendering Voxels...")
20 | for y in range(1, rbmp.height):
21 | print("y =", y)
22 | if not rt.progressupdate(100.0 * y / rbmp.height):
23 | break
24 | pixel_line = rt.getPixels(rbmp, rt.Point2(0, y-1), rbmp.width)
25 | z_line = rt.getPixels(z_d, rt.Point2(0, y-1), rbmp.width)
26 | for x in range(1, rbmp.width):
27 | print("x =", x, z_line[x].value)
28 | box = rt.box(width=10, length=10, height=z_line[x].value/2)
29 | box.pos = rt.Point3(x*10, -y*10, 0)
30 | box.wirecolor = pixel_line[x]
31 | box.name = rt.uniqueName("VoxelBox")
32 | rt.progressEnd()
33 | rt.renderers.current = prev_renderer
34 |
35 | def startup():
36 | """
37 | Hook the function to a menu item.
38 | """
39 | menuhook.register(
40 | "zdepthchannel",
41 | "howtos",
42 | zdepthchannel,
43 | menu=["&Scripting", "Python3 Development", "How To"],
44 | text="Access the Z-Depth Channel",
45 | tooltip="Access the Z-Depth Channel",
46 | in2025_menuid=menuhook.HOW_TO,
47 | id_2025="BC1B51C9-2F02-496D-B9ED-9A61022A569D")
48 |
--------------------------------------------------------------------------------
/src/packages/speedsheet/speedsheet/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | speedsheet example: Output Object Data to File
3 | """
4 | import menuhook
5 | import pymxs
6 | from pymxs import runtime as rt
7 |
8 | def speedsheet():
9 | '''Output Object Data To File'''
10 | output_name = rt.getSaveFileName(
11 | caption="SpeedSheet File",
12 | types="SpeedSheet (*.ssh)|*.ssh|All Files (*.*)|*.*|")
13 | if output_name is not None:
14 | with open(output_name, "w+", encoding="utf-8") as output_file:
15 | with pymxs.attime(rt.animationRange.start):
16 | objdump = ", ".join(map(lambda x: x.name, list(rt.selection)))
17 | output_file.write(
18 | f"Object(s): {objdump}\n")
19 | average_speed = 0
20 | for t in range(int(rt.animationRange.start), int(rt.animationRange.end)):
21 | with pymxs.attime(t):
22 | current_pos = rt.selection.center
23 | with pymxs.attime(t-1):
24 | last_pos = rt.selection.center
25 | frame_speed = rt.distance(current_pos, last_pos) * rt.FrameRate
26 | average_speed += frame_speed
27 | output_file.write(f"Frame {t}: {frame_speed}\n")
28 | average_speed /= float(rt.animationRange.end - rt.animationRange.start)
29 | output_file.write(f"Average Speed: {average_speed}\n")
30 | rt.edit(output_name)
31 |
32 | def startup():
33 | """
34 | Hook the function to a menu item.
35 | """
36 | menuhook.register(
37 | "speedsheet",
38 | "howtos",
39 | speedsheet,
40 | menu=["&Scripting", "Python3 Development", "How To"],
41 | text="Save object data to file",
42 | tooltip="Save object data to file",
43 | in2025_menuid=menuhook.HOW_TO,
44 | id_2025="FFA6888A-B27A-4FA9-BFED-86FD3683E6B6")
45 |
--------------------------------------------------------------------------------
/scripts/create.sh:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env bash
2 | # create a new pip package for a 3ds Max feature
3 | set -e
4 | script="$(dirname "$(readlink -f "$0")")"
5 |
6 | if [ $# -lt 1 ]
7 | then
8 | echo "please provide the name of the sample to create"
9 | exit 1
10 | fi
11 |
12 | samplename=$1
13 | sampledescr=${2:-$samplename sample}
14 |
15 | if [ -e "$samplename" ]
16 | then
17 | echo "the directory already exists. please rm -f $samplename if you want to reset it"
18 | exit 1
19 | fi
20 |
21 |
22 | mkdir -p "$samplename/$samplename"
23 |
24 | cat >"$samplename/LICENSE" <"$samplename/README.md" <"$samplename/setup.py" <>$samplename/$samplename/__init__.py <= number >= fl:
77 | f = fc if number == fl else 0
78 | t = tc if number == tl else len(line)
79 | line = line[0:f] + " " * (t-f) + line[t:]
80 | return line
81 |
82 | return '\n'.join([blankline(l, i) for i, l in enumerate(lines)])
83 |
--------------------------------------------------------------------------------
/src/packages/transformlock/README.md:
--------------------------------------------------------------------------------
1 | # HowTo: transformlock
2 |
3 | [Original MaxScript Tutorial](https://help.autodesk.com/view/MAXDEV/2022/ENU/?guid=GUID-8EB13535-72B4-439C-94D3-E93434BA163B)
4 | [Source Code](transformlock/__init__.py)
5 |
6 | *Goals:*
7 | - learn how to call a function in pymxs
8 | - learn how to hook a Python function to a 3ds Max ui element
9 |
10 | ## Explanations
11 |
12 | The shows how to create a minimal Python tool for 3ds Max. This tools adds a menu item
13 | to the Scripting menu to lock all transformations on the selection.
14 |
15 | ## Using the tool
16 |
17 | From the 3ds Max listener window we can do:
18 |
19 | ```python
20 | import transformlock
21 |
22 | transformlock.startup()
23 | ```
24 |
25 | If we install this sample as a pip package it will be automatically
26 | started during the startup of 3ds Max (because it defines a startup
27 | entry point for 3ds Max).
28 |
29 | ## Understanding the code
30 |
31 | We first import menuhook. This package provides an easy way to create a menu item
32 | for running a Python function.
33 |
34 | ```python
35 | import menuhook
36 | ```
37 |
38 | We also import pymxs. Pymxs lets Python access the whole MAXScript scripting library.
39 |
40 | ```python
41 | from pymxs import runtime as rt
42 | ```
43 |
44 | The core business logic of the program comes from the lock\_selection function. This uses
45 | the setTransformLockFlags of ([node common methods](https://help.autodesk.com/view/MAXDEV/2022/ENU/?guid=GUID-D1D7EB56-A370-4B07-99B4-BC779FB87CAF#GUID-D1D7EB56-A370-4B07-99B4-BC779FB87CAF__SECTION_130281B392F64446B4AE8562EAD75531))
46 | to lock all (`rt.Name("all")` transforms on the whole selection (`rt.selection`)):
47 |
48 | ```python
49 | def lock_selection():
50 | '''Lock all transforms on the selection'''
51 | rt.setTransformLockFlags(rt.selection, rt.Name("all"))
52 | ```
53 |
54 | We then define the startup function. This function will be called by the 3ds Max
55 | entry point (if this project is installed as a pip package). We can also call it
56 | manually (in the listener window) if we prefer.
57 |
58 | ```python
59 | def startup():
60 | """
61 | Hook the transform lock function to a menu item.
62 | """
63 | menuhook.register(
64 | "transformlock",
65 | "howtos",
66 | lock_selection,
67 | menu=["&Scripting", "Python3 Development", "How To"],
68 | text="Lock transformations for the selection",
69 | tooltip="Lock transformations for the selection")
70 | ```
71 |
72 |
--------------------------------------------------------------------------------
/src/samples/pymxs/animation.py:
--------------------------------------------------------------------------------
1 | '''
2 | Demonstrates simple animation.
3 | '''
4 | from pymxs import runtime as rt # pylint: disable=import-error
5 | import pymxs as mx # pylint: disable=import-error
6 |
7 | print("Hello World Animation")
8 |
9 | def print_interval(interval):
10 | '''Prints an animation interval'''
11 | print(f"Current Animation Range: [{interval.start},{interval.end}]")
12 |
13 | def set_animation_ranges():
14 | '''Changes the animation range from the default of 100 frames to 200 frames'''
15 | frames = 200
16 | print_interval(rt.animationRange)
17 | rt.animationRange = rt.Interval(0, frames)
18 | # The animation slider now shows 200 frames
19 | print_interval(rt.animationRange)
20 |
21 |
22 | def animate_transform(thing):
23 | '''Records an animation on the provided object'''
24 | # select the object to animate so we will see the keyframes in the timeslider
25 | rt.select(thing)
26 |
27 | # animate
28 | with mx.animate(True):
29 | with mx.redraw(True):
30 | with mx.attime(30):
31 | thing.pos = rt.Point3(50, 0, 0)
32 |
33 | with mx.attime(60):
34 | thing.Pos = rt.Point3(100, 50, 0)
35 |
36 | with mx.attime(90):
37 | thing.Pos = rt.Point3(50, 100, 0)
38 |
39 | with mx.attime(120):
40 | thing.Pos = rt.Point3(0, 100, 0)
41 |
42 | with mx.attime(150):
43 | thing.Pos = rt.Point3(-50, 50, 0)
44 |
45 | with mx.attime(180):
46 | thing.Pos = rt.Point3(0, 0, 0)
47 |
48 | def playback_animation():
49 | '''Play back the animation 3 times'''
50 | rt.playbackLoop = False
51 | # play animation
52 | print("Playing back Animation first time")
53 | rt.sliderTime = 0
54 | rt.timeConfiguration.playbackSpeed = 3 # normal speed
55 | rt.playAnimation()
56 |
57 | # replay it
58 | print("Playing back Animation second time")
59 | rt.timeConfiguration.playbackSpeed = 4 # double speed
60 | rt.sliderTime = 0
61 | rt.playAnimation()
62 |
63 | # replay it, faster
64 | print("Playing back Animation third time, faster")
65 | rt.sliderTime = 0
66 | rt.timeConfiguration.playbackSpeed = 5 # 4x speed
67 | rt.playAnimation()
68 |
69 | def demo_animation():
70 | '''Show how to do animation'''
71 | rt.resetMaxFile(rt.Name('noPrompt'))
72 | sphere = rt.sphere()
73 | set_animation_ranges()
74 | animate_transform(sphere)
75 | playback_animation()
76 |
77 | demo_animation()
78 |
--------------------------------------------------------------------------------
/src/samples/pymxs/render.py:
--------------------------------------------------------------------------------
1 | """
2 | Demonstrate scene rendering with pymxs.
3 | """
4 | import os
5 | import math
6 | import pymxs # pylint: disable=import-error
7 | from pymxs import runtime as rt # pylint: disable=import-error
8 |
9 | INST = rt.Name("instance")
10 |
11 | def create_spheres():
12 | '''Create a scene made of spiralling spheres.'''
13 | sphere = rt.sphere(radius=6.0)
14 | revolutions = 9 * 360
15 | radius = 40.0
16 | z_sphere = 0.0
17 | # cloning the original sphere to create the spiral effect
18 | for i in range(0, revolutions, 20):
19 | # the maxscript CloneNodes method accepts a named argument called 'newNodes'
20 | # the argument must be sent by reference as it serves as an output argument
21 | # since the argument is not also an input argument, we can simply initialize
22 | # the byref() object as 'None'
23 | # the output argument along with the call result is then returned in a tuple
24 | # note: 'newNodes' returns an array of cloned nodes
25 | # in the current case, only one element is cloned
26 | result, nodes = rt.MaxOps.CloneNodes(sphere, cloneType=INST, newNodes=pymxs.byref(None))
27 | radians = math.radians(i)
28 | x_sphere = radius * math.cos(radians)
29 | y_sphere = radius * math.sin(radians)
30 | # note: 'newNodes' returned an array of cloned nodes
31 | # in the current case, only one element is cloned
32 | nodes[0].Position = rt.Point3(x_sphere, y_sphere, z_sphere)
33 | z_sphere += 1.0
34 | radius -= 0.20
35 |
36 | def maximize_perspective():
37 | '''Setup perspective for the render'''
38 | rt.viewport.setLayout(rt.Name('layout_1'))
39 | rt.viewport.setType(rt.Name('view_persp_user'))
40 | rt.viewport.setTM(
41 | rt.matrix3(
42 | rt.point3(0.707107, 0.353553, -0.612372),
43 | rt.point3(-0.707107, 0.353553, -0.612372),
44 | rt.point3(0, 0.866025, 0.5),
45 | rt.point3(-0.00967026,-70.3466,-552.481)
46 | )
47 | )
48 |
49 | def render():
50 | '''Render in the renderoutput directory.'''
51 | output_path = os.path.join(rt.getDir(rt.Name("renderoutput")), 'foo.jpg')
52 | if os.path.exists(output_path):
53 | os.remove(output_path)
54 | rt.render(outputFile=output_path)
55 |
56 | def demo_render():
57 | '''Create a demo scene, adjust the perspective and render the scene'''
58 | rt.resetMaxFile(rt.Name('noPrompt'))
59 | create_spheres()
60 | maximize_perspective()
61 | render()
62 |
63 | demo_render()
64 |
--------------------------------------------------------------------------------
/scripts/checks.sh:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env bash
2 | set -e
3 | script="$(dirname "$(readlink -f "$0")")"
4 | packagedir="$script/../src/packages"
5 | workdir=$(pwd)
6 | IFS=$'\n'
7 | pl=pylint
8 |
9 | lintdir() {
10 | comment="$1"
11 | folder="$2"
12 | echo "$comment"
13 | $pl "$folder"
14 | # also prevent runtime.execute
15 | if grep -n -R -E "runtime\.execute\(|rt.execute\(" --include '*.py' "$folder"
16 | then
17 | echo "pymxs.execute used"
18 | exit 1
19 | fi
20 | }
21 | lint() {
22 | for f in $(find "$packagedir" -name "setup.py")
23 | do
24 | local package="$(basename "$(dirname "$f")")"
25 | lintdir "$package" "$packagedir/$package/$package"
26 | done
27 | }
28 |
29 | lintsamples() {
30 | lintdir "samples" "$script/../src/samples"
31 | }
32 |
33 | checkmarkdown() {
34 | # find code blocks in markdown that don't specify the language
35 | git grep -n '```' -- "*.md" |
36 | dos2unix |
37 | awk 'NR % 2 == 1' |
38 | grep '``` *$' |
39 | sed "s/$/ code block does not specify language/g"
40 |
41 | # find references to max that are not 3ds Max
42 | git grep "[^a-zA-Z_]max[^a-zA-Z]" -- "*.py" "*.md" |
43 | sed "s/$/ uses max instead of 3ds Max/g"
44 | git grep -n "[^a-zA-Z_\`.]python[^a-zA-Z_]" -- "*.md" |
45 | sed "s/$/: python should be spelled Python/g"
46 | }
47 |
48 | checkmdlinks() {
49 | file=$1
50 | filedir=$(dirname "$file")
51 | # iterate all links in the file
52 | for link in $(grep -n -o "\[[^]]*\]([^)]*)" "$file")
53 | do
54 | url=$(echo "$link" | sed -e "s/^[^:]*://" -e "s/\[[^]]*\]//" -e "s/^(//" -e "s/)$//")
55 | line=$(echo "$link" | grep -o "^[^:]*")
56 | if [[ "$url" =~ ^https?:.* ]]
57 | then
58 | if ! curl --output /dev/null --silent --head "$url"
59 | then
60 | echo "$file:$line: Broken url (no head): $url"
61 | fi
62 | elif [[ "$url" =~ ^/.* ]]
63 | then
64 | if [ ! -e "$workdir$url" ]
65 | then
66 | echo "$file$line: Broken absolute link: $url"
67 | fi
68 | else
69 | if [ ! -e "$filedir/$url" ]
70 | then
71 | echo "$file:$line: Broken relative link: $url"
72 | fi
73 | fi
74 | done
75 | }
76 |
77 | checkmarkdownlinks() {
78 | # iterate all matching markdown files in the repo
79 | for f in $(git grep --name-only "\[[^]]*\]([^)]*)" -- "*.md")
80 | do
81 | checkmdlinks "$f"
82 | done
83 | }
84 |
85 |
86 | lint
87 | lintsamples
88 | checkmarkdown
89 | checkmarkdownlinks
90 |
--------------------------------------------------------------------------------
/doc/uninstall.md:
--------------------------------------------------------------------------------
1 | # Uninstalling the HowTos
2 |
3 | It is easy undo the work of the installation scripts. The steps needed
4 | to uninstall everything are explained here.
5 |
6 | ## uninstall.sh
7 |
8 | The `uninstall.sh` script will:
9 |
10 | - uninstall pip
11 | - uninstall the pip packages of the samples
12 | - remove pystartup.ms
13 |
14 | The menu items in 3ds Max will not be removed automatically (the steps
15 | needed to remove them are explained at the bottom of this page).
16 |
17 | ## Removing pystartup.ms (manual uninstall)
18 |
19 | ### For 3dsMax before 2025
20 | After the installation, pystartup.ms will be copied to:
21 |
22 | "$HOME/AppData/Local/Autodesk/3dsMax/2022 - 64bit/ENU/scripts/startup"
23 |
24 | It can simply be removed from there.
25 |
26 | > Note: pystartup.ms finds the pip packages
27 | > in the Python environment. If they expose a 3dsMax startup entry point
28 | > the entry point is called during the 3ds Max startup.
29 |
30 | By removing this file none of the HowTo packages will be started
31 | automatically when 3ds Max starts.
32 |
33 | ### For 3dsMax 2025 and greater
34 |
35 | Starting with 2025, pystartup.ms is no longer needed. Instead the
36 | [adn-devtech-python-howtos](/src/adn-devtech-python-howtos) directory is
37 | copied to "C:\ProgramData\Autodesk\ApplicationPlugins".
38 |
39 | It can be manually removed by doing (from gitbash):
40 |
41 | ```bash
42 | rm -fr "$ProgramData/Autodesk/ApplicationPlugins/adn-devtech-python-howtos"
43 | ```
44 |
45 | ## Removing pip (manual uninstall)
46 |
47 | The installation script also install pip in user mode.
48 |
49 | You can also remove pip (although this is not really recommended).
50 | To remove it:
51 |
52 | ```bash
53 | # current directory needs to be maxinstallationfolder/Python37:
54 | ./python.exe -m pip uninstall pip
55 | ```
56 |
57 | ## Removing the individual HowTos (manual uninstall)
58 |
59 | The HowTos can be uninstalled individually by calling:
60 |
61 | ```bash
62 | # current directory needs to be maxinstallationfolder/Python37:
63 | ./python.exe -m pip uninstall reloadmod-autodesk
64 | ```
65 |
66 | > Note that the full package name should be the name of the
67 | > package directory followed by `-autodesk`.
68 |
69 | ## Uninstalling all the HowTos at Once (manual uinstall)
70 |
71 | The [uninstallhowtos.sh](/uninstallhowtos.sh) can be used
72 | to uninstall all the howtos at once. This will automatically call
73 | `./python.exe -m pip uninstall` for all the HowTos packages.
74 |
75 | ## Getting rid of the menu items and the action items in 3ds Max
76 |
77 | Menu items and action items added to 3ds Max are permanent.
78 | Cleaning up the menu items needs to be done manually from the
79 | user interface (Customize > Customize User Interface > Menus).
80 |
81 |
--------------------------------------------------------------------------------
/src/packages/inbrowserhelp/README.md:
--------------------------------------------------------------------------------
1 | # HowTo: inbrowserhelp
2 |
3 | This sample shows how to open a page in the webbrowser from Python
4 |
5 | ## Explanations
6 |
7 | The example creates a submenu in the Scripting -> Python3 Development menu,
8 | called Browse Documentation. Each menu item in this menu opens the platform
9 | browser in a location that may be useful to Python developers.
10 |
11 | # Understanding the code
12 |
13 | The standard `webbrowser` module is imported first:
14 |
15 | ```python
16 | import webbrowser
17 | ```
18 |
19 | The `menuhook` module is then imported. This module makes it easier to create
20 | menu items in the 3ds Max menu system:
21 |
22 | ```python
23 | import menuhook
24 | ```
25 |
26 | The topics that need to be added in the submenu are then declared in a list
27 | of tuples. The first element is a unique name for the item, the second
28 | element is a description to be displayed in menu items, and the third element
29 | is the url (without https://, this will be added later, forcing https and not
30 | http to be used for everything).
31 |
32 | ```python
33 | from pymxs import runtime as rt
34 | MAX_VERSION = rt.maxversion()[7]
35 | MAX_HELP = f"help.autodesk.com/view/MAXDEV/{MAX_VERSION}/ENU"
36 |
37 | TOPICS = [
38 | ("gettingstarted", "Getting Started With Python in 3ds Max",
39 | f"{MAX_HELP}/?guid=Max_Python_API_tutorials_creating_the_dialog_html"),
40 | ("howtos", "Python HowTos Github Repo",
41 | "github.com/ADN-DevTech/3dsMax-Python-HowTos"),
42 | ("samples", "Python samples (Github Repo)",
43 | "github.com/ADN-DevTech/3dsMax-Python-HowTos/tree/master/src/samples"),
44 | ("pymxs", "Pymxs Online Documentation",
45 | f"{MAX_HELP}/?guid=Max_Python_API_using_pymxs_html"),
46 | ("pyside2", "Qt for Python Documentation (PySide2)",
47 | "doc.qt.io/qtforpython/contents.html"),
48 | ("python", "Python 3.7 Documentation",
49 | "docs.python.org/3.7/")
50 | ]
51 | ```
52 |
53 | The MENU\_LOCATION constant gives the location in the menu system where the
54 | new items need to be added:
55 |
56 | ```python
57 | MENU_LOCATION = ["&Scripting", "Python3 Development", "Browse Documentation"]
58 | ```
59 |
60 | With this, it is now possible to iterate the list of TOPICS and for each
61 | topic to create a menu item:
62 |
63 | ```python
64 | for topic in TOPICS:
65 | menuhook.register(
66 | f"inbrowserhelp_{topic[0]}",
67 | "howtos",
68 | lambda topic=topic: webbrowser.open(f"https://{topic[2]}",
69 | MENU_LOCATION,
70 | text=topic[1],
71 | tooltip=topic[1])
72 | ```
73 |
74 | Opening a topic simply consists in calling `webbrowser.open(f"https://{topic[2]}")`
75 | when the menu item is activated.
76 |
--------------------------------------------------------------------------------
/src/samples/pymxs/materials.py:
--------------------------------------------------------------------------------
1 | '''
2 | Demonstrates how to iterate through materials and and apply them to objects.
3 | It shows how to open the material editor and put materials in the editor.
4 | '''
5 | from pymxs import runtime as rt # pylint: disable=import-error
6 |
7 | def create_floor():
8 | """Create a rectangle for the floor"""
9 | plane = rt.Plane()
10 | plane.width = 120
11 | plane.length = 120
12 |
13 | def print_material_properties(material_instance):
14 | """Print the properties of a given material"""
15 | print("[%s]" % material_instance.name)
16 | for name in rt.getPropNames(material_instance):
17 | print("\t" + name + " = " + str(rt.getProperty(material_instance, name)))
18 |
19 | def create_text(xpos, ypos, rot, message):
20 | """Create a visible label on the ground for a given teapot"""
21 | tex = rt.text()
22 | tex.size = 10
23 | tex.text = message
24 | tex.position = rt.Point3(xpos, ypos, 0)
25 | tex.rotation = rot
26 | tex.wireColor = rt.Color(255, 128, 255)
27 |
28 | def showcase_materials(materials):
29 | """Create a teapot sample and a visible label for each provided material"""
30 | num_materials = len(materials)
31 | diff = 360.0 / num_materials
32 | teapot_radius = 5.0
33 | radius = 50.0
34 | text_radius = 90.0
35 | index = 0
36 | i = 0
37 |
38 | for mat in materials:
39 | position = rt.Point3(radius, 0, 0)
40 | rot = rt.angleAxis(i, rt.Point3(0, 0, 1))
41 |
42 | teapot = rt.teapot()
43 | teapot.radius = teapot_radius
44 | teapot.position = position
45 | teapot.rotation = rot
46 | teapot.Material = mat
47 | print_material_properties(mat)
48 |
49 | create_text(text_radius, 0, rot, mat.name)
50 | if index < 24:
51 | rt.setMeditMaterial(index + 1, mat)
52 | index += 1
53 | i += diff
54 |
55 | def sample():
56 | """Create all existing materials and showcase them."""
57 | def try_create(mat):
58 | """Try to create a given material. If not creatable return None."""
59 | try:
60 | return mat()
61 | except RuntimeError:
62 | return None
63 | rt.resetMaxFile(rt.Name('noPrompt'))
64 | # maximize the view (select a view with only the one viewport)
65 | rt.viewport.setLayout(rt.name("layout_1"))
66 | # show the material editor in basic mode
67 | rt.MatEditor.mode = rt.Name("basic")
68 | rt.MatEditor.open()
69 | # create a plane for the floor
70 | create_floor()
71 | # instantiate all materials that can be instantiated
72 | materials = filter(lambda x: x is not None, map(try_create, rt.material.classes))
73 | # showcase all materials
74 | showcase_materials(list(materials))
75 |
76 | sample()
77 |
--------------------------------------------------------------------------------
/src/packages/reloadmod/reloadmod/reload.py:
--------------------------------------------------------------------------------
1 | """
2 | Reload multiple modules using importlib.
3 | """
4 | import sys
5 | import importlib
6 | import inspect
7 | import pymxs
8 |
9 | FORCE_SKIP = []
10 |
11 | def non_builtin():
12 | """Return a set of all modules names that are not builtins and not
13 | importlib related."""
14 | skip = set(
15 | # filter out the builtins that should not be reloaded anyway
16 | list(sys.builtin_module_names) +
17 | # filter out importlib that if reloaded breaks the whole thing
18 | list(filter(lambda k: k.find("importlib") >= 0, sys.modules.keys())) +
19 | FORCE_SKIP)
20 | return set(filter(lambda k: not (k in skip) and not is_builtin(k), sys.modules.keys()))
21 |
22 | #pylint: disable=broad-except
23 | def reload_many(keys):
24 | """Reload multiple packages by name"""
25 | for k in keys:
26 | try:
27 | importlib.invalidate_caches()
28 | importlib.reload(sys.modules[k])
29 | print(f"module {k} reloaded")
30 | except NotImplementedError:
31 | print(f" *module {k} could not be reloaded because Not Implemented")
32 | except Exception as ex:
33 | print(f" *module {k} could not be reloaded because {str(ex)}")
34 | #pylint: enable=broad-except
35 |
36 | def is_builtin(key):
37 | """Test builtin using inspect (some modules not seen
38 | as builtin in sys.builtin_module_names may look builtin
39 | anyway to inspect and in this case we want to filter them
40 | out."""
41 | try:
42 | inspect.getfile(sys.modules[key])
43 | except TypeError:
44 | return True
45 | return False
46 |
47 | def module_path(key):
48 | """Return the loading path of a module"""
49 | return inspect.getfile(sys.modules[key])
50 |
51 | def prefixed_by(apath, some_paths):
52 | """Check if a path is a a subpath of one of the provided paths"""
53 | return len([p for p in some_paths if apath.find(p) == 0]) > 0
54 |
55 | def filter_out_paths(keys, fullpaths):
56 | """Filter out paths prefixed by some path"""
57 | return {k for k in keys if not prefixed_by(module_path(k), fullpaths)}
58 |
59 | def filter_out_string(keys, string):
60 | """Filter out paths that contain a specific string"""
61 | return {k for k in keys if module_path(k).find(string) < 0}
62 |
63 | def show_location(title, keys):
64 | """Show the location of a set of packages"""
65 | print(title)
66 | for k in keys:
67 | print(f"{k} loaded from {module_path(k)}")
68 |
69 | def non_max():
70 | """Return a set of packages that are not 3ds Max related"""
71 | return filter_out_paths(non_builtin(), [pymxs.runtime.getdir(pymxs.runtime.name("maxroot"))])
72 |
73 | def dev_only():
74 | """Return a set of packages that are not dev related"""
75 | return filter_out_string(non_max(), "site-packages")
76 |
--------------------------------------------------------------------------------
/src/packages/pyconsole/README.md:
--------------------------------------------------------------------------------
1 | # HowTo: pyconsole
2 |
3 | 
4 |
5 | This example integrates the nice [pyqtconsole](https://github.com/marcus-oscarsson/pyqtconsole)
6 | as an alternative to the 3ds Max Listener. The console is automatically added
7 | to the "RightDockWidgetArea" and tabbed on startup. A menu item allows to
8 | create other dockable instances of the console.
9 |
10 | This console behaves a lot more like a normal python console and
11 | provides syntax highlighting and autocomplete on top of that. Because
12 | the console uses Qt, integrating it in 3ds Max is really easy.
13 |
14 | ## Understanding the code
15 |
16 | The nice stuff is in [pyqtconsole](https://github.com/marcus-oscarsson/pyqtconsole). Here
17 | I show how it is integrated to max.
18 |
19 | The new\_console function:
20 |
21 | ```python
22 | def new_console(tabto = None, floating = False, dockingarea = QtCore.Qt.RightDockWidgetArea):
23 | """
24 | Create a new console and float it as a max widget
25 | tabto: name of a widget on top of which the console should be tabbed
26 | floating: True to float the console, False to leave it docked
27 | ```
28 |
29 |
30 | Retrieves the main 3dsMax windows. This is a Qt window.
31 |
32 | ```python
33 | main_window = GetQMaxMainWindow()
34 | ```
35 |
36 | It then instanciates a new console:
37 | ```python
38 | # create and setup a console
39 | console = PythonConsole(formats=HUGOS_THEME)
40 | console.setStyleSheet("background-color: #333333;")
41 | ```
42 |
43 | And creates a QDockWidget as a container for the console.
44 |
45 | ```python
46 | # create a dock widget for the console
47 | dock_widget = QDockWidget(main_window)
48 | dock_widget.setWidget(console)
49 | dock_widget.setObjectName("pyconsole")
50 | dock_widget.setWindowTitle("Python Console")
51 | main_window.addDockWidget(dockingarea, dock_widget)
52 | ```
53 |
54 | The widget is docked by default and optionally tabbed if
55 | tabto is specified:
56 |
57 | ```python
58 | if (not tabto is None):
59 | tabw = main_window.findChild(QWidget, tabto)
60 | main_window.tabifyDockWidget(tabw, dock_widget)
61 | ```
62 |
63 | If the floating argument is true, the widget is floated instead
64 | of being docked. And then it is showed.
65 |
66 | ```python
67 | dock_widget.setFloating(floating)
68 | dock_widget.show()
69 | ```
70 |
71 | Finally, the console is hooked to the qt event processing mechanism.
72 | This is pretty awesome, no 3dsMax specific code is needed!
73 |
74 | ```python
75 | # make the console do stuff
76 | console.eval_queued()
77 | ```
78 |
79 | The awesomeness here (aside from the [pyqtconsole](https://github.com/marcus-oscarsson/pyqtconsole) that is great)
80 | is really that pymxs (or any other 3ds Max specific thing) is not even needed!
81 |
82 | The python + Qt combo in 3ds Max is really fantastic!
83 |
84 |
--------------------------------------------------------------------------------
/src/packages/quickpreview/README.md:
--------------------------------------------------------------------------------
1 | # HowTo: quickpreview
2 |
3 | 
4 |
5 | [Original MaxScript Tutorial](https://help.autodesk.com/view/MAXDEV/2022/ENU/?guid=GUID-333382D0-57AF-4797-98F2-C2BE09442607)
6 | [Source Code](quickpreview/__init__.py)
7 |
8 | *Goals:*
9 | - learn to record preview frames of an animation
10 |
11 | ## Explanations
12 |
13 | - Define an AVI output path name in the Previews system directory.
14 | - Get the size of the current viewport.
15 | - Create a bitmap with the size of the viewport and set the file name to the output path .
16 | - Loop through all animation frames in the current segment.
17 | - Set the slider to the time from the loop.
18 | - Capture the viewport Device Independent Bitmap.
19 | - Copy the DIB to the pre-defined bitmap.
20 | - Save the current frame to disk.
21 | - When ready with all frames, close the bitmap.
22 | - Force garbage collection manually to clear any used memory.
23 | - Load the ready animation in the RAM player.
24 |
25 | ## Using the tool
26 |
27 | From the 3ds Max listener window we can do:
28 |
29 | ```python
30 | import quickpreview
31 |
32 | quickpreview.startup()
33 | ```
34 |
35 | If we install this sample as a pip package it will be automatically
36 | started during the startup of 3ds Max (because it defines a startup
37 | entry point for 3ds Max).
38 |
39 | ## Understanding the code
40 |
41 | We use `rt.getDir(rt.Name("preview"))` to find the default directory
42 | for previews ([3ds Max System Directories](https://help.autodesk.com/view/MAXDEV/2022/ENU/?guid=GUID-F7577416-051E-478C-BB5D-81243BAAC8EC#GUID-F7577416-051E-478C-BB5D-81243BAAC8EC)).
43 | We concatenate "quickpreview.avi" using the standard Python `path.join` function.
44 |
45 | ```python
46 | preview_name = path.join(rt.getDir(rt.Name("preview")), "quickpreview.avi")
47 | ```
48 |
49 | We then retrieve the view size, and create a bitmap that matches this size
50 | in the preview\_name file:
51 |
52 | ```pyhton
53 | view_size = rt.getViewSize()
54 | anim_bmp = rt.bitmap(view_size.x, view_size.y, filename=preview_name)
55 | ```
56 |
57 | We then iterate over the animationRange, creating a bitmap from the viewport at
58 | each frame. This bitmap is copied to the anim\_bmp that we created and then
59 | saved.
60 |
61 | ```python
62 | for t in range(int(rt.animationRange.start), int(rt.animationRange.end)):
63 | rt.sliderTime = t
64 | dib = rt.gw.getViewportDib()
65 | rt.copy(dib, anim_bmp)
66 | rt.save(anim_bmp)
67 | ```
68 |
69 | After the loop is over we close the anim\_bmp file that now contains our
70 | animation.
71 |
72 | ```python
73 | rt.close(anim_bmp)
74 | ```
75 |
76 | We then force a garbage collection:
77 |
78 | ```python
79 | rt.gc()
80 | ```
81 |
82 | And launch the [RAMPlayer](https://help.autodesk.com/view/MAXDEV/2022/ENU/?guid=GUID-650BE5AA-1DFB-4847-99B2-777A281490F6#GUID-650BE5AA-1DFB-4847-99B2-777A281490F6) for our generated avi:
83 |
84 | ```python
85 | rt.ramplayer(preview_name, "")
86 | ```
87 |
88 |
--------------------------------------------------------------------------------
/src/samples/PySide/test_ui.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | TabWidget
4 |
5 |
6 |
7 | 0
8 | 0
9 | 374
10 | 243
11 |
12 |
13 |
14 | TabWidget
15 |
16 |
17 | 0
18 |
19 |
20 |
21 |
22 | 0
23 | 0
24 |
25 |
26 |
27 | Tab 1
28 |
29 |
30 |
31 | QLayout::SetNoConstraint
32 |
33 |
34 | 3
35 |
36 |
37 | 3
38 |
39 |
40 | 3
41 |
42 |
43 | 3
44 |
45 | -
46 |
47 |
-
48 |
49 |
50 | PushButton
51 |
52 |
53 |
54 | -
55 |
56 |
57 | ...
58 |
59 |
60 |
61 |
62 |
63 | -
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | 0
72 | 0
73 |
74 |
75 |
76 | Tab 2
77 |
78 |
79 |
80 | 3
81 |
82 |
83 | 3
84 |
85 |
86 | 3
87 |
88 |
89 | 3
90 |
91 | -
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/src/samples/pymxs/mxs_token.py:
--------------------------------------------------------------------------------
1 | '''
2 | Demonstrates the use of mxstoken
3 | '''
4 | import threading
5 | import time
6 | import pymxs # pylint: disable=import-error
7 |
8 | def mxstoken_sample():
9 | '''
10 | Demonstrate 3 threads using mxstoken.
11 | '''
12 | flag = True
13 | counter = 0
14 |
15 | def call_mxs_entry():
16 | '''
17 | Access pymxs in a mxstoken protected block of code.
18 | '''
19 | with pymxs.mxstoken():
20 | pymxs.runtime.Teapot()
21 |
22 | def call_mxs_entry_ex_1(locker, tick, evt):
23 | '''
24 | Demonstrate a first function with a mix of concurrent code
25 | and mxstoken protected code.
26 | '''
27 | try:
28 | locker.acquire()
29 | nonlocal flag, counter
30 | flag = False
31 | with pymxs.mxstoken():
32 | pymxs.runtime.Teapot(Name="call_mxs_entry_ex_1")
33 | # give up lock, let ex_2 could exec codes
34 | locker.release()
35 | if not evt.wait(tick):
36 | pymxs.print_(
37 | "Error: event untriggered\n" +
38 | "which indicates 'with block' in ex_2 haven't finished\n",
39 | True,
40 | True)
41 | counter = 30
42 | except:
43 | pymxs.print_("Error: unexpected exception\n", True, True)
44 | raise
45 | finally:
46 | if locker.locked():
47 | locker.release()
48 |
49 | def call_mxs_entry_ex_2(locker, tick, evt):
50 | '''
51 | Demonstrate a second function with a mix of concurrent code
52 | and mxstoken protected code.
53 | '''
54 | nonlocal flag, counter
55 | while flag:
56 | time.sleep(tick)
57 |
58 | try:
59 | locker.acquire()
60 | # we expected this block is finished
61 | # before ex_1 wakeup from sleep
62 | for _ in range(10):
63 | # only a indicator, could just assign counter = 10
64 | counter = counter + 1
65 | evt.set()
66 | with pymxs.mxstoken():
67 | # this block won't be executed after ex_1 with block finished
68 | pymxs.runtime.Teapot(Name="call_mxs_entry_ex_2")
69 | if counter != 30:
70 | pymxs.print_(
71 | ("Error: expected counter 30, got %d" +
72 | "which indicates 'with block' in ex_2 haven't finished\n").format(counter),
73 | True,
74 | True)
75 | finally:
76 | if locker.locked():
77 | locker.release()
78 | pymxs.print_("success\n", False, True)
79 |
80 | # Steps:
81 | locker = threading.Lock()
82 | evt = threading.Event()
83 | thread1 = threading.Thread(target=call_mxs_entry)
84 | thread2 = threading.Thread(target=call_mxs_entry_ex_1, args=(locker, 1, evt))
85 | thread3 = threading.Thread(target=call_mxs_entry_ex_2, args=(locker, 0.01, evt))
86 | thread1.start()
87 | thread2.start()
88 | thread3.start()
89 |
90 | mxstoken_sample()
91 |
--------------------------------------------------------------------------------
/src/samples/pymxs/mesh_and_cpv.py:
--------------------------------------------------------------------------------
1 | '''
2 | Demonstrates how to create a mesh from scratch and to set color per vertex data.
3 | '''
4 | from pymxs import runtime as rt # pylint: disable=import-error
5 |
6 | def set_edge_visibility(mesh, face, aedge, bedge, cedge):
7 | """Set the visibility of face edges"""
8 | rt.setEdgeVis(mesh, face, 1, aedge)
9 | rt.setEdgeVis(mesh, face, 2, bedge)
10 | rt.setEdgeVis(mesh, face, 3, cedge)
11 |
12 | def make_pyramid_mesh(side=20.0):
13 | """Create a pyramid from vertices and faces."""
14 | mesh = rt.mesh()
15 | mesh.numverts = 4
16 | mesh.numfaces = 4
17 |
18 | halfside = side / 2.0
19 | rt.SetVert(mesh, 1, rt.Point3(0.0, 0.0, side))
20 | rt.SetVert(mesh, 2, rt.Point3(-halfside, -halfside, 0.0))
21 | rt.SetVert(mesh, 3, rt.Point3(-halfside, halfside, 0.0))
22 | rt.SetVert(mesh, 4, rt.Point3(halfside, 0.0, 0.0))
23 |
24 | rt.setFace(mesh, 1, 1, 2, 3)
25 | set_edge_visibility(mesh, 1, True, True, False)
26 |
27 | rt.setFace(mesh, 2, 1, 3, 4)
28 | set_edge_visibility(mesh, 2, True, True, False)
29 |
30 | rt.setFace(mesh, 3, 1, 4, 2)
31 | set_edge_visibility(mesh, 2, True, True, False)
32 |
33 | rt.setFace(mesh, 4, 2, 3, 4)
34 | set_edge_visibility(mesh, 2, True, True, False)
35 |
36 | rt.update(mesh)
37 | return mesh
38 |
39 | def output_channel(mesh, channel, name):
40 | """Retrieve and display the information about a given mesh map."""
41 | print("Channel: " + name)
42 | if not rt.meshop.getMapSupport(mesh, channel):
43 | print(" Not enabled")
44 | return
45 |
46 | vertices = rt.meshop.getNumMapVerts(mesh, channel)
47 | print(f" Number of texture vertices: {vertices}")
48 | for vindex in range(1, vertices + 1):
49 | vertex = rt.meshop.getMapVert(mesh, channel, vindex)
50 | print(f" Texture vertex {vertex.X}, {vertex.Y}, {vertex.Z}")
51 |
52 | faces = rt.meshop.getNumMapFaces(mesh, channel)
53 | print(f" Number of faces: {faces}")
54 | for findex in range(1, faces + 1):
55 | face = rt.meshop.getMapFace(mesh, channel, findex)
56 | print(f" Texture vertex indices {face.X}, {face.Y}, {face.Z}")
57 | print()
58 |
59 | def output_channels(mesh):
60 | """Retrieve and display the information about all channels."""
61 | nummaps = rt.meshop.getNumMaps(mesh)
62 | print(f"NumMaps: {nummaps}")
63 | print()
64 | output_channel(mesh, 0, "color per vertex")
65 | output_channel(mesh, 1, "texture mapping")
66 |
67 | def main():
68 | """Create a mesh, color it, and output information about its maps."""
69 | # reset the scene
70 | rt.resetMaxFile(rt.Name('noPrompt'))
71 | # create a mesh
72 | mesh = make_pyramid_mesh()
73 | print("Updating the color per vertex channel")
74 | rt.setNumCPVVerts(mesh, 2)
75 | rt.buildVCFaces(mesh)
76 | rt.setVertColor(mesh, 1, rt.Color(255, 0, 0))
77 | rt.setVertColor(mesh, 2, rt.Color(0, 0, 255))
78 | rt.setVCFace(mesh, 1, rt.Point3(1, 1, 2))
79 | rt.setVCFace(mesh, 2, rt.Point3(1, 2, 2))
80 | rt.setVCFace(mesh, 3, rt.Point3(2, 2, 2))
81 | rt.setVCFace(mesh, 4, rt.Point3(1, 1, 1))
82 | rt.setCVertMode(mesh, True)
83 | rt.update(mesh)
84 | output_channels(mesh)
85 |
86 | main()
87 |
--------------------------------------------------------------------------------
/src/packages/renameselected/README.md:
--------------------------------------------------------------------------------
1 | # HowTo: renameselected
2 |
3 | 
4 |
5 | [Original MaxScript Tutorial](https://help.autodesk.com/view/MAXDEV/2022/ENU/?guid=GUID-5986CAD3-BB68-47BC-B4B2-EF84C4659271)
6 | [Source Code](renameselected/__init__.py)
7 |
8 | *Goals:*
9 | - learn how to create a dialog with PySide
10 | - learn how to hook a Python function to a 3ds Max ui element
11 |
12 | ## Explanations
13 |
14 | This tutorial shows how to rename all selected objects using a base name,
15 | chosen in a PySide dialog.
16 |
17 | ## Using the tool
18 |
19 | From the 3ds Max listener window we can do:
20 |
21 | ```python
22 | import renameselected
23 |
24 | renameselected.startup()
25 | ```
26 |
27 | Then from the Scripting menu we can use the renameselected menu item to use
28 | the feature.
29 |
30 | If we install this sample as a pip package it will be automatically
31 | started during the startup of 3ds Max (because it defines a startup
32 | entry point for 3ds Max).
33 |
34 | ## Understanding the code
35 |
36 | The renaming is done in the `renameselected` function that receives a base name
37 | in its text parameter. Then `rt.uniquename(text)` is used for generating a unique
38 | version of this base name for each item in the selection.
39 |
40 | ```python
41 | def renameselected(text):
42 | '''Rename all elements in selection'''
43 | if text != "":
44 | for i in rt.selection:
45 | i.name = rt.uniquename(text)
46 | ```
47 |
48 | This function is called whenever the `Rename selected objects` button of a dialog
49 | is pressed. This dialog is implemented in [ui.py](renameselected/ui.py).
50 |
51 | We declare a QDialog subclass, and make sure it receives a click function in its
52 | constructor. This click function will be our renameselected function:
53 |
54 | ```python
55 | class PyMaxDialog(QDialog):
56 | """
57 | Custom dialog attached to the 3ds Max main window
58 | """
59 | def __init__(self, click, parent=QWidget.find(rt.windows.getMAXHWND())):
60 | ```
61 |
62 | We then populate the dialog with various widgets:
63 |
64 | ```python
65 | super(PyMaxDialog, self).__init__(parent)
66 | self.setWindowTitle('Rename')
67 |
68 | main_layout = QVBoxLayout()
69 | label = QLabel("Enter new base name")
70 | main_layout.addWidget(label)
71 |
72 | edit = QLineEdit()
73 | main_layout.addWidget(edit)
74 |
75 | btn = QPushButton("Rename selected objects")
76 | ```
77 |
78 | And make sure its push button calls the click function when clicked:
79 |
80 | ```python
81 | btn.clicked.connect(lambda : click(edit.text()))
82 | main_layout.addWidget(btn)
83 |
84 | self.setLayout(main_layout)
85 | self.resize(250, 100)
86 | ```
87 |
88 | Finally, in [__init__.py](renameselected/__init__.py) we create a menu item that
89 | will show the dialog.
90 |
91 | ```python
92 | def startup():
93 | """
94 | Hook the function to a menu item.
95 | """
96 | menuhook.register(
97 | "howtos",
98 | "renameselected",
99 | showdialog,
100 | menu=[ "&Scripting", "Python3 Development", "How To"],
101 | text="renameselected sample",
102 | tooltip="renameselected sample")
103 | ```
104 |
105 | The showdialog function simply does this:
106 |
107 | ```python
108 | def showdialog():
109 | dialog = ui.PyMaxDialog(renameselected)
110 | dialog.show()
111 | ```
112 |
--------------------------------------------------------------------------------
/src/packages/threadprogressbar/README.md:
--------------------------------------------------------------------------------
1 | # HowTo: threadprogressbar
2 |
3 | 
4 |
5 | *Goal:*
6 | - learn how to update a progress bar from a worker thread
7 |
8 | *Non Goal:*
9 | - explaining how to connect a Python function to a menu item (this is done
10 | in other samples like [removeallmaterials](/src/packages/removeallmaterials/README.md))
11 |
12 | ## Explanations
13 |
14 | The tutorial creates a menu item that opens a dialog. When the dialog
15 | is started it launches a thread that does some processing in
16 | the background and notifies the dialog of its progress. A progress bar
17 | shows the percentage of completion of the background job.
18 |
19 | Important remark: pymxs is not thread safe. Python threads cannot
20 | call into pymxs. It is possible for Python threads to emit QT Signals
21 | that will be processed on the main thread, and this mechanism allows
22 | both UI and pymxs interatction during thread execution.
23 |
24 | ## Using the tool
25 |
26 | From the 3ds Max listener window we can do:
27 |
28 | ```python
29 | import threadprogressbar
30 |
31 | threadprogressbar.startup()
32 | ```
33 |
34 | If we install this sample as a pip package it will be automatically
35 | started during the startup of 3ds Max (because it defines a startup
36 | entry point for 3ds Max).
37 |
38 | ## Understanding the code
39 |
40 | In [threadprogressbar/\_\_init\_\_.py](threadprogressbar/__init__.py) we
41 | create a menu item. This menu item calls the following function that simply
42 | creates and shows a dialog.
43 |
44 | ```python
45 | def threadprogressbar():
46 | '''Update a progress bar from a thread'''
47 | dialog = ui.PyMaxDialog()
48 | dialog.show()
49 | ```
50 |
51 | The dialog is defined in [threadprogressbar/ui.py](threadprogressbar/ui.py). This
52 | file also defines a worker thread, that has a signal and an aborted flag:
53 |
54 | ```python
55 | class Worker(QThread):
56 | progress = Signal(int)
57 | aborted = False
58 | def __init__(self):
59 | QThread.__init__(self)
60 |
61 | ```
62 |
63 | Its run function loops from MINRANGE to MAXRANGE, waiting 0.5 second per
64 | iteration. On each iteration it checks if the aborted flag was set and
65 | exits if it's the case. It also emits the current value of the iteration
66 | on its progress signal:
67 |
68 | ```python
69 | for i in range(MINRANGE, MAXRANGE):
70 | self.progress.emit(i)
71 | time.sleep(0.5)
72 | if self.aborted:
73 | return
74 | self.progress.emit(MAXRANGE)
75 | ```
76 |
77 | The PySide dialog creates a bunch of widgets including a QProgressBar
78 | that shows the progress of the worker, and a QPushButton that allows
79 | to abort the worker.
80 |
81 | ```python
82 | # progress bar
83 | pb = QProgressBar()
84 | pb.minimum = MINRANGE
85 | pb.maximum = MAXRANGE
86 | main_layout.addWidget(pb)
87 |
88 | # abort button
89 | btn = QPushButton("abort")
90 | main_layout.addWidget(btn)
91 | ```
92 |
93 | It then starts the worker and connects its progress signal to the
94 | setValue function of the pogressbar.
95 |
96 | ```python
97 | # start the worker
98 | self.worker = Worker()
99 | self.worker.progress.connect(pb.setValue)
100 | self.worker.start()
101 | ```
102 |
103 | Finally it connects the clicked signal of the abort button to the
104 | abort function of the worker:
105 |
106 | ```python
107 | # connect abort button
108 | btn.clicked.connect(self.worker.abort)
109 | ```
110 |
--------------------------------------------------------------------------------
/doc/pluginpackage.md:
--------------------------------------------------------------------------------
1 | # Plugin Packages in 2025 and Integration With the New Menu System
2 |
3 | In 3ds Max 2025 and above, plugin packages may contain python script components.
4 | When installing the samples from this repo in a 2025 version of 3ds Max,
5 | the [adn-devtech-python-howtos](/src/adn-devtech-python-howtos) plugin package
6 | is copied to "$ProgramData/Autodesk/ApplicationPlugins". The [PackageContents.xml](/src/adn-devtech-python-howtos/PackageContents.xml)
7 | file of this plugin package declares a python pre-start-up script:
8 |
9 | ```xml
10 |
11 |
12 |
13 |
14 | ```
15 |
16 | The [./scripts/pyStartup.py](/src/adn-devtech-python-howtos/scripts/pyStartup.py) script
17 | first does what [pystartup.ms](/src/pystartup/pystartup.ms) used to do:
18 |
19 | ```python
20 | def _python_startup():
21 | try:
22 | import pkg_resources
23 | except ImportError:
24 | print('startup Python modules require pip to be installed.')
25 | return
26 | for dist in pkg_resources.working_set:
27 | entrypt = pkg_resources.get_entry_info(dist, '3dsMax', 'startup')
28 | if not (entrypt is None):
29 | try:
30 | fcn = entrypt.load()
31 | fcn()
32 | except Exception as e:
33 | print(f'skipped package startup for {dist} because {e}, startup not working')
34 |
35 | ```
36 |
37 | Then it integrates the samples into the new menu system of 2025:
38 |
39 | ```python
40 | # configure 2025 menus
41 | from pymxs import runtime as rt
42 | from menuhook import register_howtos_menu_2025
43 | def menu_func():
44 | menumgr = rt.callbacks.notificationparam()
45 | register_howtos_menu_2025(menumgr)
46 |
47 | # menu system
48 | cuiregid = rt.name("cuiRegisterMenus")
49 | howtoid = rt.name("pyScriptHowtoMenu")
50 | rt.callbacks.removescripts(id=cuiregid)
51 | rt.callbacks.addscript(cuiregid, menu_func, id=howtoid)
52 |
53 | ```
54 |
55 |
56 | ## Integration With the New Menu System
57 |
58 | The [menuhook](/src/menuhook/) code has been reworked to integrate menu items for the
59 | various howtos into the new menu system:
60 |
61 | ```python
62 | def register(action, category, fcn, menu=None, text=None, tooltip=None, in2025_menuid=None, id_2025=None):
63 | ```
64 |
65 | Takes two new parameters:
66 | - `in2025_menuid` : the guid of the containing menu
67 | - `id_2025` : the guid of the item to create
68 |
69 | And stores the needed menu items in the `registered_items` list:
70 |
71 | ```python
72 | registered_items.append((in2025_menuid, id_2025, category, action))
73 | ```
74 |
75 | The `register_howotos_menu_2025` function, called whenever the menu system needs to regenerate its structure, uses the `registered_items` list to add items to the menu manager:
76 |
77 | ```python
78 | # hook the registered items
79 | for reg in registered_items:
80 | (in2025_menuid, id_2025, category, action) = reg
81 | scriptmenu = menumgr.getmenubyid(in2025_menuid)
82 | if scriptmenu is not None:
83 | try:
84 | actionitem = scriptmenu.createaction(id_2025, 647394, f"{action}`{category}")
85 | except Exception as e:
86 | print(f"Could not create item {category}, {action} in menu {in2025_menuid} because {e}")
87 | else:
88 | print(f"Could not create item {category}, {action}, in missing menu {in2025_menuid}")
89 |
90 |
91 | ```
92 |
--------------------------------------------------------------------------------
/scripts/inst.sh:
--------------------------------------------------------------------------------
1 | set -e
2 | script="$(dirname "$(readlink -f "$0")")"
3 | installdir="$(pwd)"
4 | packagedir="$script/src/packages"
5 |
6 | # we need to know the max version. Normally this should be part of the current install dir
7 | # but it can be defined manually with the VERSION environment variable before calling
8 | # the scripts. If it is not it will be inferred from the installation directory
9 | dirversion=$(pwd | grep -o '20[0-9]\{2\}' || echo "")
10 | version=${VERSION:-$dirversion}
11 | if [ -z "$version" ]
12 | then
13 | # Take the latest ADSK_3DSMAX_x64 dir
14 | v=$(env | grep 'ADSK_3DSMAX_x64_20[0-9]\{2\}' | sed 's/=.*$//; s/^ADSK_3DSMAX_x64_//' | sort -n -r | head -n 1)
15 | if (( "$v" > 2021 ))
16 | then
17 | version=$v
18 | else
19 | echo "3ds Max Version number could not be inferred from the installation directory"
20 | echo "The VERSION env variable can be set before calling this script to define the 3ds Max Version (ex: VERSION=2022)"
21 | exit 1
22 | fi
23 | fi
24 |
25 | localsettings="$HOME/AppData/Local/Autodesk/3dsMax"
26 | if [ ! -f "$installdir/installSettings.ini" ]
27 | then
28 | startuppath="$localsettings/$version - 64bit/ENU/scripts/startup"
29 | elif grep -i "installedBuild=1" "$installdir/installSettings.ini" >/dev/null 2>&1
30 | then
31 | startuppath="$localsettings/$version - 64bit/ENU/scripts/startup"
32 | elif iconv -f UTF-16 -t UTF-8 /dev/null 2>&1
33 | then
34 | startuppath="$localsettings/$version - 64bit/ENU/scripts/startup"
35 | else
36 | startuppath="$installdir/scripts/startup"
37 | fi
38 |
39 | if [ "$version" -le "2022" ]
40 | then
41 | pythonpath="Python37"
42 | else
43 | pythonpath="Python"
44 | fi
45 |
46 | exiterr() {
47 | echo "$@" 1>&2
48 | exit 1
49 | }
50 |
51 | # make sure curl is available
52 | if ! command -v curl >/dev/null 2>&1
53 | then
54 | exiterr "curl needs to be in the path."
55 | fi
56 |
57 | # install pip
58 | installpip() {
59 | cd "$installdir/$pythonpath"
60 | if ! ./python.exe -m pip -V 2>/dev/null
61 | then
62 | if ! ./python.exe -m ensurepip 2>/dev/null
63 | then
64 | local getpip="$(mktemp -d -t tbdXXXXXXXX)"
65 | curl "https://bootstrap.pypa.io/get-pip.py" > "$getpip/get-pip.py"
66 | ./python.exe "$getpip/get-pip.py" --user
67 | fi
68 | fi
69 | # update to the latest pip
70 | ./python.exe -m pip install --upgrade pip
71 | ./python.exe -m pip install wheel
72 | }
73 |
74 | # install pystartup.ms or adn-devtech-python-howtos (plugin package) for 2025
75 | installpystartup() {
76 | if [ "$version" -lt "2025" ]
77 | then
78 | cp "$script/src/pystartup/pystartup.ms" "$startuppath"
79 | else
80 | cp -fr "$script/src/adn-devtech-python-howtos" "$ProgramData/Autodesk/ApplicationPlugins"
81 | fi
82 | }
83 |
84 |
85 | # install all Python packages in the repo with the -e option
86 | installpythonpackages() {
87 | for f in $(find "$packagedir" -name "setup.py")
88 | do
89 | local package="$(dirname "$f")"
90 | "$installdir/$pythonpath/python.exe" -m pip install --user -e "$package"
91 | done
92 | }
93 |
94 | # uninstall all Python packages in the repo
95 | uninstallpythonpackages() {
96 | for f in $(find "$packagedir" -name "setup.py")
97 | do
98 | local package="$(basename "$(dirname "$f")")"
99 | local pname="$package-autodesk"
100 | "$installdir/$pythonpath/python.exe" -m pip uninstall -y "$pname"
101 | done
102 | }
103 |
104 |
--------------------------------------------------------------------------------
/src/samples/PySide/docking_widgets.py:
--------------------------------------------------------------------------------
1 | '''
2 | Demonstrates how to create a QWidget with PySide and attach it to the 3dsmax main window.
3 | Creates two types of dockable widgets, a QDockWidget and a QToolbar
4 | '''
5 |
6 | import os
7 | import ctypes
8 |
9 | from qtpy import QtCore
10 | from qtpy import QtGui
11 | from qtpy.QtWidgets import QMainWindow, QDockWidget, QToolButton, QToolBar, QAction
12 |
13 | from pymxs import runtime as rt
14 | from qtmax import GetQMaxMainWindow
15 |
16 | def get_pos_to_dock_toolbar(dock_widget):
17 | """
18 | Get the docking widget position based on its size
19 | """
20 | space_between_widgets = 20 # Arbritrary hard coded value
21 | dock_widget_rect = dock_widget.geometry()
22 | x_pos = dock_widget_rect.x()
23 | y_pos = dock_widget_rect.bottom() + space_between_widgets
24 | return QtCore.QPoint(x_pos, y_pos)
25 |
26 | def make_toolbar_floating(toolbar, pos):
27 | """
28 | Set the toolbar widget properties to act as a tool floating window
29 | """
30 | toolbar.setWindowFlags(
31 | QtCore.Qt.Tool | QtCore.Qt.FramelessWindowHint | QtCore.Qt.X11BypassWindowManagerHint)
32 | toolbar.move(pos)
33 | toolbar.adjustSize()
34 | toolbar.show()
35 | QtCore.QMetaObject.invokeMethod(toolbar, \
36 | "topLevelChanged", \
37 | QtCore.Qt.DirectConnection, \
38 | QtCore.QGenericArgument("bool", ctypes.c_void_p(True)))
39 |
40 | def create_cylinder():
41 | """
42 | Create a cylinder node with predetermined radius and height values.
43 | """
44 | rt.Cylinder(radius=10, height=30)
45 | rt.redrawViews()
46 |
47 | def demo_docking_widgets():
48 | """
49 | Demonstrates how to create a QWidget with PySide and attach it to the 3dsmax main window.
50 | Creates two types of dockable widgets, a QDockWidget and a QToolbar
51 | """
52 | # Retrieve 3ds Max Main Window QWdiget
53 | main_window = GetQMaxMainWindow()
54 |
55 | # QAction reused by both dockable widgets.
56 | cylinder_icon_path = os.path.dirname(os.path.realpath(__file__)) + "\\cylinder_icon_48.png"
57 | cylinder_icon = QtGui.QIcon(cylinder_icon_path)
58 | create_cyl_action = QAction(cylinder_icon, u"Create Cylinder", main_window)
59 | create_cyl_action.triggered.connect(create_cylinder)
60 |
61 | # QDockWidget construction and placement over the main window
62 | dock_widget = QDockWidget(main_window)
63 |
64 | # Set for position persistence
65 | dock_widget.setObjectName("Creators")
66 | # Set to see dock widget name in toolbar customize popup
67 | dock_widget.setWindowTitle("Creators")
68 | dock_tool_button = QToolButton()
69 | dock_tool_button.setAutoRaise(True)
70 | dock_tool_button.setDefaultAction(create_cyl_action)
71 | dock_tool_button.setToolButtonStyle(QtCore.Qt.ToolButtonTextOnly)
72 | dock_widget.setWidget(dock_tool_button)
73 |
74 | main_window.addDockWidget(QtCore.Qt.LeftDockWidgetArea, dock_widget)
75 | dock_widget.setFloating(True)
76 | dock_widget.show()
77 |
78 | # QToolBar construction and attachement to main window
79 | toolbar_widget = QToolBar(main_window)
80 |
81 | # Set for position persistence
82 | toolbar_widget.setObjectName("Creators TB")
83 | # Set to see dock widget name in toolbar customize popup
84 | toolbar_widget.setWindowTitle("Creators TB")
85 | toolbar_widget.setFloatable(True)
86 | toolbar_widget.addAction(create_cyl_action)
87 |
88 | main_window.addToolBar(QtCore.Qt.BottomToolBarArea, toolbar_widget)
89 | toolbar_widget.show()
90 |
91 | toolbar_position = get_pos_to_dock_toolbar(dock_widget)
92 | make_toolbar_floating(toolbar_widget, toolbar_position)
93 |
94 | demo_docking_widgets()
95 |
--------------------------------------------------------------------------------
/src/pystartup/README.md:
--------------------------------------------------------------------------------
1 | # pystartup
2 |
3 | `pystartup.ms` is a MAXScript file that adds to 3ds Max the ability to startup
4 | pip packages when they are present in the Python environment.
5 |
6 |
7 | ## Installation
8 | The pystartup.ms file must be copied to the 3dsMax startup directory.
9 |
10 | ## How to make a pip package "auto start"
11 |
12 | To be automatically loaded by this mechanism, a pip package must
13 | implement the 3dsMax startup entry point.
14 |
15 |
16 | This is done by adding a line like this in the setup.py file of
17 | a pip package:
18 |
19 | ```python
20 | entry_points={'3dsMax': 'startup=yourpackagename:startup'},
21 | ```
22 |
23 | where `yourpackagename` is the name of the package (i.e. import
24 | yourpackagename works) and startup is an exported function of
25 | yourpackagename that will be called during startup.
26 |
27 | Most if not all Python samples in this repo implement this entry
28 | point. [transformlock's setup script](/src/packages/transformlock/setup.py) can
29 | be taken as en example as well as [transformlock's \_\_init\_\_.py](/src/packages/transformlock/transformlock/__init__.py).
30 |
31 |
32 | ## The maxscript code
33 |
34 | The pystartup.ms code consists in a call to python.execute that runs
35 | a small Python program.
36 |
37 | ```maxscript
38 | if isProperty python "execute" then (
39 | python.execute ("def _python_startup():\n" +
40 | " try:\n" +
41 | " import pkg_resources\n" +
42 | " except ImportError:\n" +
43 | " print('startup Python modules require pip to be installed.')\n" +
44 | " return\n" +
45 | " for dist in pkg_resources.working_set: \n" +
46 | " entrypt = pkg_resources.get_entry_info(dist, '3dsMax', 'startup')\n" +
47 | " if not (entrypt is None):\n" +
48 | " try:\n" +
49 | " fcn = entrypt.load()\n" +
50 | " fcn()\n" +
51 | " except:\n" +
52 | " print('skipped package startup for {}, startup not working'.format(dist))\n" +
53 | "_python_startup()\n" +
54 | "del _python_startup")
55 | )
56 | ```
57 |
58 | ## The Python code
59 |
60 | Here is the Python code used for the entry points:
61 |
62 | ```python
63 | def _python_startup():
64 | try:
65 | import pkg_resources
66 | except ImportError:
67 | print('startup Python modules require pip to be installed.')
68 | return
69 | for dist in pkg_resources.working_set:
70 | entrypt = pkg_resources.get_entry_info(dist, '3dsMax', 'startup')
71 | if not (entrypt is None):
72 | try:
73 | fcn = entrypt.load()
74 | fcn()
75 | except:
76 | print(f'skipped package startup for {dist}, startup not working')
77 | _python_startup()
78 | del _python_startup
79 | ```
80 |
81 | `pkg\_resources` is part of [setuptools](https://setuptools.readthedocs.io/en/latest/pkg_resources.html)
82 | and comes installed with pip. It allows this script to discover packages present
83 | in the environment.
84 |
85 | The script first imports pkg\_resources.
86 |
87 | ```python
88 | import pkg_resources
89 | ```
90 |
91 | it then iterates all packages in the environment:
92 |
93 | ```python
94 | for dist in pkg_resources.working_set:
95 | ```
96 |
97 | It then tries to find the 3dsMax startup entry point in the package:
98 |
99 | ```python
100 | entrypt = pkg_resources.get_entry_info(dist, '3dsMax', 'startup')
101 | ```
102 |
103 | And if this works, loads and executes the entry point:
104 |
105 | ```python
106 | if not (entrypt is None):
107 | try:
108 | fcn = entrypt.load()
109 | fcn()
110 | except:
111 | print(f'skipped package startup for {dist}, startup not working')
112 | ```
113 |
--------------------------------------------------------------------------------
/doc/install.md:
--------------------------------------------------------------------------------
1 | # Installation
2 |
3 | It is possible to install the samples in 3ds Max. This
4 | will add a Python3 scripting menu to 3ds Max:
5 |
6 | 
7 |
8 | The examples and some development goodies will be made available from there.
9 |
10 | The installation does the following:
11 | - it installs pip in your 3ds Max installation if it's not already there
12 | - it installs pystartup.ms that enables auto start pip packages
13 | - it installs all the samples in --user and -e mode with pip
14 |
15 | If you decide to install the howtos, it is highly recommended that you clone
16 | this git repository locally using git bash (whenever we update the samples,
17 | you will be able to update your local version and re-run the installation scripts):
18 |
19 | ```bash
20 | # from the directory where you want the sample
21 | git clone https://github.com/ADN-DevTech/3dsMax-Python-HowTos.git
22 | ```
23 |
24 | Also note that *all installation steps decribed here also use git bash* (it is
25 | possible to use another client for git but all installation scripts in
26 | this repo use bash).
27 |
28 | ### Option A: Install Everthing Locally in One Step (--user)
29 | > Note: the steps described here need to be done from a git bash prompt
30 |
31 | The [install.sh](/install.sh) script can be used from bash
32 | to install the samples in 3ds Max. The script needs to be run from a
33 | 3ds Max installation directory.
34 |
35 | ### Option B: Install Everything Locally in Two Steps (--user)
36 | > Note: the steps described here need to be done from a git bash prompt
37 |
38 | It is possible to break up the installation in two steps.
39 |
40 | - The [installstartup.sh](/installstartup.sh) script can be used
41 | from bash to install pip and [pystartup.ms](/src/pystartup/pystartup.ms).
42 | In 2025 and above, [adn-devtech-python-howtos](/src/adn-devtech-python-howtos)
43 | is installed instead of pystartup.ms.
44 |
45 | It needs to run in the 3ds Max installation directory.
46 |
47 | You may do only this step if you don't want the HowTos but you
48 | want to install pip and pystartup.ms.
49 |
50 | - The [installhowtos.sh](/installhowtos.sh) script can be used from
51 | bash to pip install all the howtos in `--user` mode and `-e` mode (--user
52 | means that the samples will be intalled under `~/AppData/Roaming/Python/Python37/site-packages/`,
53 | and -e means that the packages will be installed as symlinks to the
54 | source directories so that if the sources change the packages don't need
55 | to be reinstalled).
56 | This script needs to run in the 3ds Max installation directory.
57 |
58 | ## Uninstalling the HowTos
59 |
60 | The steps needed to uninstall the HowTos can be found in [uninstall.md](/doc/uninstall.md).
61 |
62 | ### Option C: Install the howtos in a virtual environment
63 | > Note: the steps described here need to be done from a git bash prompt
64 |
65 | This last option requires three steps.
66 |
67 | It can be used to install the HowTos in a virtual environment (ex:
68 | you may want to have a virtual environment for Python development).
69 |
70 | - The first step is the same as the first step described in option B:
71 | [installstartup.sh](/installstartup.sh) needs to run in the 3ds Max
72 | installation directory to install pip if it is missing and pystartup.
73 |
74 | - The second step consists in installing virtualenv with pip and creating a
75 | virtual environment. These steps are described in the [3ds Max documentation](https://help.autodesk.com/view/MAXDEV/2022/ENU/?guid=Max_Python_API_python_3_support_virtual_env_html).
76 | - The last step consists in installing the HowTos in the virtual environment.
77 | From the same git bash prompt, the [installhowtos.sh](/installhowtos.sh)
78 | script can be used to install the HowTos in a virtual environment. First `cd`
79 | to the directory of the virtual env and then (without activating the env) simply
80 | run [installhowtos.sh](/installhowtos.sh) from that directory.
81 |
82 |
83 |
--------------------------------------------------------------------------------
/src/packages/mxs2py/mxs2py/mxs2py.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python3
2 | """
3 | Program to transfom maxscript code to python code.
4 | """
5 | # pylint: disable=invalid-name, import-error
6 | import re
7 | import os
8 | import sys
9 | from parsec import ParseError
10 | from mxs2py import mxsp
11 | from mxs2py import pyout
12 | from mxs2py import mxscp
13 | #from mxs2py.log import eprint
14 |
15 | def preprocess(inputbuf: str, filename: str) -> str:
16 | """
17 | Preprocess the inputbuf, replaceing \r\n by \n, etc.
18 | """
19 | dirname = os.path.dirname(filename)
20 | # pretty bogus just strip out include directives (what we should do is read the
21 | # refernced file and insert it. Would not be more difficult but let's keep it simple for now)
22 | includeregex = re.compile(r'include +"([^"]*)"')
23 | def expandmatch(mpath: str) -> str:
24 | # pylint: disable=invalid-name, line-too-long
25 | """Read file mpath, replacing \r\n by \n and \t by four spaces and returning the resulting buffer"""
26 | fn = mpath[1]
27 | fullfn = os.path.join(dirname, fn)
28 | with open(fullfn, encoding="utf-8") as f:
29 | buf = f.read().replace("\r\n", "\n").replace("\t", " ")
30 | return (mpath[0], buf)
31 |
32 | for path_rep in map(expandmatch, re.finditer(includeregex, inputbuf)):
33 | inputbuf = inputbuf.replace(path_rep[0], path_rep[1])
34 |
35 | return inputbuf
36 |
37 |
38 | def topy(inputstr, file_header=None, snippet=False):
39 | """
40 | Convert some mxs inputstr to py.
41 | """
42 | #eprint("------ input mxs program ----")
43 | #eprint(inputstr)
44 | # we don't want to mix the comments whith the
45 | # syntax tree. So we parse the comments first,
46 | # and then the syntax tree, and keep locations
47 | # if we want to keep the comments when producing
48 | # the output we are able
49 | #eprint("------ parse comments -------")
50 | comments = mxscp.anycomment.parse(inputstr)
51 | #eprint(comments)
52 | #eprint("------ replace comments with white space")
53 | stripped = mxscp.blank_comments(inputstr, comments)
54 | #eprint(stripped)
55 | #eprint("------- parse tree ------")
56 | parsed = mxsp.file.parse(stripped)
57 | #eprint(parsed[1])
58 | lines = stripped.split("\n")
59 | numlines = len(lines)
60 | output = pyout.out_py(parsed[1], comments, file_header, snippet)
61 | parsedlines, dummy = parsed[2]
62 | parsedlines = parsedlines + 1
63 | error = None
64 | if parsedlines < numlines:
65 | error = f"partial parse parsedlines = {parsedlines}, numlines = {numlines} {parsed}\n"
66 | toreparse = '\n'.join(lines[parsedlines:])
67 | try:
68 | mxsp.program_step.parse(toreparse)
69 | except ParseError as e:
70 | error = f"{error}\n\n{e}\n\nin:\n\n{toreparse}"
71 |
72 | #eprint("------- output ----------")
73 | #eprint(output)
74 | #eprint("------- done ----------")
75 | return (output, error)
76 |
77 | def main():
78 | """
79 | Main program
80 | All (optional) args are file name.
81 | If no args are provided the code operates on stdin.
82 | """
83 | if len(sys.argv) > 1:
84 | for fname in sys.argv[1:]:
85 | with open(fname, encoding="utf-8") as f:
86 | buf = f.read().replace("\r\n", "\n")
87 | buf = preprocess(buf, fname)
88 | with open("outfile", "w", encoding="utf-8") as of:
89 | of.write(buf)
90 | (output, error) = topy(buf, f"Automatically converted {fname}")
91 | if error is not None:
92 | sys.exit(-1)
93 | print(output)
94 | else:
95 | inputstr = sys.stdin.read()
96 | (output, error) = topy(inputstr, "Automatically converted stdin")
97 | if error is not None:
98 | sys.exit(-1)
99 | print(output)
100 | sys.exit(0)
101 |
102 | if __name__ == "__main__":
103 | main()
104 |
--------------------------------------------------------------------------------
/src/packages/mxs2py/README.md:
--------------------------------------------------------------------------------
1 | # mxs2py: a maxscript to python transpiler
2 | 
3 |
4 | mxs2py takes a MAXScript program, parses it to a syntax tree, and then produces equivalent Python
5 | code from the syntax tree.
6 |
7 | ## Why?
8 |
9 | Help python programmers make sense of maxscript samples by providing automatic translation
10 | to python.
11 |
12 | ## State of the Project
13 |
14 | This is experimental work
15 |
16 | As things are:
17 |
18 | - most correct maxcript syntax will be parsed correctly
19 | - parsing errors are poorly reported (trying to translate invalid maxscript code will not be a fun
20 | experience)
21 | - some maxscript constructs are still not supported by the code generator. Most of the time, when things
22 | cannot be translated to python, a warning is emitted in the generated code explaining why.
23 | - constructs like plugin, tool, custom attributes, etc. are parsed but then the python support is either
24 | inexistant or limited
25 | - some generated python constructions may be correct but awkward (because implementing it this way was
26 | easier)
27 |
28 | ## Example
29 |
30 | The following maxscript code from [the maxcript howtos](https://help.autodesk.com/view/MAXDEV/2022/ENU/?guid=GUID-3667A33C-E3E4-4F39-A480-3713240838F1)
31 |
32 | ```maxscript
33 | renderers.current = Default_Scanline_Renderer()
34 | delete $VoxelBox*
35 | rbmp = render outputsize:[32,32] channels:#(#zdepth) vfb:off
36 | z_d = getchannelasmask rbmp #zdepth
37 | progressstart "Rendering Voxels..."
38 | for y = 1 to rbmp.height do
39 | (
40 | if not progressupdate (100.0 * y / rbmp.height) then exit
41 | pixel_line = getpixels rbmp [0,y-1] rbmp.width
42 | z_line = getpixels z_d [0,y-1] rbmp.width
43 | for x = 1 to rbmp.width do
44 | (
45 | b = box width:10 length:10 height:(z_line[x].value/2)
46 | b.pos = [x*10,-y*10,0]
47 | b.wirecolor = pixel_line[x]
48 | b.name = uniquename "VoxelBox"
49 | )--end x loop
50 | )--end y loop
51 | progressend()
52 | ```
53 |
54 | Will be translated into the following python code:
55 | (which is essentially the same as [zdepthchannel](https://github.com/ADN-DevTech/3dsMax-Python-HowTos/tree/master/src/packages/zdepthchannel),
56 | but mechanically translated).
57 |
58 | ```python
59 | '''Converted from MAXScript to Python with mxs2py'''
60 | from pymxs import runtime as rt
61 | import mxsshim
62 | rt.renderers.current = rt.default_scanline_renderer()
63 | rt.delete(mxsshim.path("$VoxelBox*"))
64 | rbmp = rt.render(outputsize=rt.point2(32, 32), channels=rt.array(rt.name("zdepth")), vfb=False)
65 | z_d = rt.getchannelasmask(rbmp, rt.name("zdepth"))
66 | rt.progressstart("Rendering Voxels...")
67 | for y in range(int(1), 1 + int(rbmp.height)):
68 | if not rt.progressupdate(100.0 * y / rbmp.height):
69 | break
70 | pixel_line = rt.getpixels(rbmp, rt.point2(0, y - 1), rbmp.width)
71 | z_line = rt.getpixels(z_d, rt.point2(0, y - 1), rbmp.width)
72 | for x in range(int(1), 1 + int(rbmp.width)):
73 | b = rt.box(width=10, length=10, height=(z_line[x - 1].value / 2))
74 | b.pos = rt.point3(x * 10, -y * 10, 0)
75 | b.wirecolor = pixel_line[x - 1]
76 | b.name = rt.uniquename("VoxelBox")
77 | # end x loop
78 | # end y loop
79 | rt.progressend()
80 | ```
81 |
82 | Which works producing :
83 |
84 | 
85 |
86 | ## Under the hood
87 | - The code uses parsec to parse maxscript syntax into a syntax tree
88 | - It then appliees various transformations on the syntax tree (not in a very efficient way, but in a relatively simple way)
89 | - It then emits python code from this syntax tree
90 |
91 | ## How to use it
92 |
93 | ```
94 | from mxs2py import topy
95 |
96 | (output, _) = topy("rotate $ (angleaxis 90 [1,0,0])")
97 | print(output)
98 |
99 | ```
100 |
101 | this will print:
102 |
103 | ```python
104 | '''Converted from MAXScript to Python with mxs2py'''
105 | from pymxs import runtime as rt
106 | import mxsshim
107 | import pymxs
108 | rt.rotate(mxsshim.path("$"), rt.angleaxis(90, rt.point3(1, 0, 0)))
109 | ```
110 |
111 | Note that the generated code depends on [mxsshim.py](mxsshim.py)
112 | which is a very early and incomplete emulation layer of maxscript
113 | constructs in python.
114 |
--------------------------------------------------------------------------------
/src/packages/speedsheet/README.md:
--------------------------------------------------------------------------------
1 | # HowTo: speedsheet
2 |
3 | 
4 |
5 | [Original MaxScript Tutorial](https://help.autodesk.com/view/MAXDEV/2022/ENU/?guid=GUID-2DB3A775-776F-4D63-BDFB-D99523ECB69D)
6 | [Source Code](speedsheet/__init__.py)
7 |
8 | *Goals:*
9 | - open a file selection dialog in 3ds Max
10 | - use file io in Python
11 | - use pymxs.attime, pymxs.runtime.selection, pymxs.runtime.animationRange
12 | - open a text file in the 3ds Max text editor
13 |
14 | Non Goal:
15 | - explaining how to connect a Python function to a menu item (this is done
16 | in other samples like [removeallmaterials](/src/packages/removeallmaterials/README.md))
17 |
18 | ## Explanations
19 |
20 | The following example will create a macroScript that will output the speed of the
21 | current object selection on each frame and its average speed to a text file.
22 |
23 | A difference with the MAXScript version of the sample is that we use Python
24 | file io instead of pymxs file io in the Python sample.
25 |
26 | ## Using the tool
27 |
28 | From the 3ds Max listener window we can do:
29 |
30 | ```python
31 | import speedsheet
32 |
33 | speedsheet.startup()
34 | ```
35 |
36 | If we install this sample as a pip package it will be automatically
37 | started during the startup of 3ds Max (because it defines a startup
38 | entry point for 3ds Max).
39 |
40 | ## Understanding the code
41 |
42 | The speedsheet function in [speedsheet/\_\_init\_\_.py](speedsheet/__init__.py) fully
43 | implements this sample.
44 |
45 | It first opens a file selection dialog, with a caption and a filter for
46 | file types, using `rt.getSaveFileName`:
47 |
48 | ```python
49 | output_name = rt.getSaveFileName(
50 | caption="SpeedSheet File",
51 | types="SpeedSheet (*.ssh)|*.ssh|All Files (*.*)|*.*|")
52 | ```
53 |
54 | It then opens the selected file for writing using the Python `open` function.
55 | The file is opened in a `with` block: this guarantees that no matter what
56 | happens in the block, the file will be closed before exiting the block. This
57 | simplifies the code and makes it more robust at the same time.
58 |
59 | ```python
60 | with open(output_name, "w+") as output_file:
61 | ```
62 |
63 | Next, the code produces a list of objects at the first frame
64 | of the animation. Everything inside the `with pymxs.attime(sometime)` block will
65 | happen at `sometime` in the time line (this is the same as the [at time](https://help.autodesk.com/view/MAXDEV/2022/ENU/?guid=GUID-4E9CCD61-F575-42E1-8654-315DDF6C6A26#GUID-4E9CCD61-F575-42E1-8654-315DDF6C6A26)
66 | construction in MAXScript):
67 |
68 | ```python
69 | with pymxs.attime(rt.animationRange.start):
70 | objdump = ", ".join(map(lambda x: x.name, list(rt.selection)))
71 | output_file.write(
72 | f"Object(s): {objdump}\n")
73 | ```
74 |
75 | The `map(lambda x: x.name, list(rt.selection))` construct converts the `rt.selection`
76 | (the set of currently selected objects in 3ds Max) in a list of strings (the name
77 | of these objects).
78 |
79 | The `", ".join()` call concatenates these strings separated by ", " to produce
80 | a nice list of object names.
81 |
82 | The code then iterates the timeline:
83 |
84 | ```python
85 | for t in range(int(rt.animationRange.start), int(rt.animationRange.end)):
86 | ```
87 |
88 | It then computes the position of the selection center at t and t-1:
89 |
90 | ```python
91 | with pymxs.attime(t):
92 | current_pos = rt.selection.center
93 | with pymxs.attime(t-1):
94 | last_pos = rt.selection.center
95 | ```
96 |
97 | From this it computes the speed at that time and writes it to the file:
98 |
99 | ```python
100 | frame_speed = rt.distance(current_pos, last_pos) * rt.FrameRate
101 | average_speed += frame_speed
102 | output_file.write(f"Frame {t}: {frame_speed}\n")
103 | ```
104 |
105 | When the loop terminates, the average speed of the animation is also logged
106 | to the file:
107 |
108 | ```python
109 | average_speed /= float(rt.animationRange.end - rt.animationRange.start)
110 | output_file.write(f"Average Speed: {average_speed}\n")
111 | ```
112 |
113 | After the end of the `with open(` block the file is closed automcatically.
114 | We open it the 3ds Max editor:
115 |
116 | ```python
117 | rt.edit(output_name)
118 | ```
119 |
--------------------------------------------------------------------------------
/src/samples/pymxs/class_types.py:
--------------------------------------------------------------------------------
1 | '''
2 | Demonstrates creating many different types of scene objects that are visible in the viewport.
3 | The scene objects are grouped by type.
4 | The types created are Cameras, Lights, Geometric Objects, Shapes, Helpers, Modifiers
5 | and Materials.
6 | '''
7 | from pymxs import runtime as rt # pylint: disable=import-error
8 | OBJECT_DIMENSION = 5.0
9 | Y_STEP = OBJECT_DIMENSION * 4
10 | X_STEP = OBJECT_DIMENSION * 2.0
11 |
12 | def create_box():
13 | """Create a box."""
14 | box = rt.box()
15 | box.Height = OBJECT_DIMENSION
16 | box.Width = OBJECT_DIMENSION
17 | box.Length = OBJECT_DIMENSION
18 | return box
19 |
20 | def create_text(pos, message):
21 | """Create a text."""
22 | tex = rt.text()
23 | tex.size = Y_STEP
24 | tex.text = message
25 | tex.position = rt.Point3(pos.x, pos.y - OBJECT_DIMENSION, pos.z)
26 | tex.wirecolor = rt.Color(255, 128, 255)
27 |
28 | def create_teapot():
29 | """Create a teapot."""
30 | teapot = rt.teapot()
31 | teapot.radius = OBJECT_DIMENSION
32 | return teapot
33 |
34 | def layout_objects(title, cases, y_position, x_offset_text=-45):
35 | """Layout a list of nodes in a line"""
36 | create_text(rt.Point3(x_offset_text, y_position, 0), title)
37 | x_position = 0.0
38 | for gen in cases:
39 | gen.Position = rt.point3(x_position, y_position, 0)
40 | x_position += X_STEP
41 | if (x_position % 260.0) < 0.001:
42 | x_position = 0.0
43 | y_position += Y_STEP
44 | return y_position
45 |
46 | def create_classes(classes):
47 | """Create all createble instances of the provided classes"""
48 | for obj in classes:
49 | try:
50 | created = obj()
51 | print(created)
52 | yield created
53 | except RuntimeError:
54 | pass
55 |
56 | def create_cameras(y_position):
57 | """Create all creatable cameras"""
58 | print("-- Cameras")
59 | return layout_objects("Cameras", create_classes(rt.camera.classes), y_position)
60 |
61 | def create_lights(y_position):
62 | """Create all creatable lights"""
63 | print("-- Lights")
64 | return layout_objects("Lights", create_classes(rt.light.classes), y_position)
65 |
66 | def create_objects(y_position):
67 | """Create all creatable objects"""
68 | print("-- Geometric Objects")
69 | return layout_objects(
70 | "Geometric Objects",
71 | create_classes(rt.GeometryClass.classes),
72 | y_position, -88.0)
73 |
74 | def create_shapes(y_position):
75 | """Create all creatable shapes"""
76 | print("-- Shapes")
77 | return layout_objects("Shapes", create_classes(rt.shape.classes), y_position)
78 |
79 | def create_helpers(y_position):
80 | """Create all creatable helpers"""
81 | print("-- Helpers")
82 | return layout_objects("Helpers", create_classes(rt.helper.classes), y_position)
83 |
84 | def create_modifiers(y_position):
85 | """Create all creatable modifiers"""
86 | def create():
87 | for mod in rt.modifier.classes:
88 | try:
89 | created = mod()
90 | print(created)
91 | box = create_box()
92 | rt.addModifier(box, created)
93 | yield box
94 | except RuntimeError:
95 | pass
96 | print("-- Modifiers")
97 | return layout_objects("Modifiers", create(), y_position)
98 |
99 | def create_materials(y_position):
100 | """Create all creatable materials"""
101 | def create():
102 | for mat in rt.material.classes:
103 | try:
104 | created = mat()
105 | print(mat)
106 | teapot = create_teapot()
107 | teapot.Material = created
108 | yield teapot
109 | except RuntimeError:
110 | pass
111 | print("-- Materials")
112 | return layout_objects("Materials", create(), y_position)
113 |
114 | def create_items():
115 | """Create all the items in the sample."""
116 | rt.resetMaxFile(rt.Name('noPrompt'))
117 | y_line = create_materials(0.0) + 40.0
118 | y_line = create_modifiers(y_line) + 40.0
119 | y_line = create_helpers(y_line) + 40.0
120 | y_line = create_shapes(y_line) + 40.0
121 | y_line = create_objects(y_line) + 40.0
122 | y_line = create_lights(y_line) + 40.0
123 | y_line = create_cameras(y_line) + 40.0
124 |
125 | create_items()
126 |
--------------------------------------------------------------------------------
/src/packages/inbrowserhelp/inbrowserhelp/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | inbrowserhelp example: inbrowserhelp sample
3 | """
4 | from sys import version_info
5 | import webbrowser
6 | from pymxs import runtime as rt
7 | import menuhook
8 | MAX_VERSION = rt.maxversion()[7]
9 | MAX_HELP = "help.autodesk.com/view/MAXDEV"
10 |
11 | PYTHON_VERSION = f"{version_info[0]}.{version_info[1]}"
12 |
13 | MAX_VERSION_TOPICS = {
14 | 2021: [
15 | ("gettingstarted",
16 | "Getting Started With Python in 3ds Max",
17 | f"{MAX_HELP}/2021/ENU/?guid=Max_Python_API_about_the_3ds_max_python_api_html",
18 | "64651C48-F4F1-42F9-8C8A-FF5D0AA031A2"),
19 | ("pymxs",
20 | "Pymxs Online Documentation",
21 | f"{MAX_HELP}/2021/ENU/?guid=Max_Python_API_using_pymxs_pymxs_module_html",
22 | "44985F87-C175-4F3D-B70F-9FA0B6242AE1")
23 | ],
24 | 2022: [
25 | ("gettingstarted",
26 | "Getting Started With Python in 3ds Max",
27 | f"{MAX_HELP}/2022/ENU/?guid=MAXDEV_Python_about_the_3ds_max_python_api_html",
28 | "64651C48-F4F1-42F9-8C8A-FF5D0AA031A2"),
29 | ("pymxs",
30 | "Pymxs Online Documentation",
31 | f"{MAX_HELP}/2022/ENU/?guid=MAXDEV_Python_using_pymxs_html",
32 | "44985F87-C175-4F3D-B70F-9FA0B6242AE1")
33 | ],
34 | 2023: [
35 | ("gettingstarted",
36 | "Getting Started With Python in 3ds Max",
37 | f"{MAX_HELP}/2023/ENU/?guid=MAXDEV_Python_about_the_3ds_max_python_api_html",
38 | "64651C48-F4F1-42F9-8C8A-FF5D0AA031A2"),
39 | ("pymxs",
40 | "Pymxs Online Documentation",
41 | f"{MAX_HELP}/2023/ENU/?guid=MAXDEV_Python_using_pymxs_html",
42 | "44985F87-C175-4F3D-B70F-9FA0B6242AE1")
43 | ],
44 | 2024: [
45 | ("gettingstarted",
46 | "Getting Started With Python in 3ds Max",
47 | f"{MAX_HELP}/2024/ENU/?guid=MAXDEV_Python_about_the_3ds_max_python_api_html",
48 | "64651C48-F4F1-42F9-8C8A-FF5D0AA031A2"),
49 | ("pymxs",
50 | "Pymxs Online Documentation",
51 | f"{MAX_HELP}/2024/ENU/?guid=MAXDEV_Python_using_pymxs_html",
52 | "44985F87-C175-4F3D-B70F-9FA0B6242AE1")
53 | ],
54 | 2025: [
55 | ("gettingstarted",
56 | "Getting Started With Python in 3ds Max",
57 | f"{MAX_HELP}/2025/ENU/?guid=MAXDEV_Python_about_the_3ds_max_python_api_html",
58 | "64651C48-F4F1-42F9-8C8A-FF5D0AA031A2"),
59 | ("pymxs",
60 | "Pymxs Online Documentation",
61 | f"{MAX_HELP}/2025/ENU/?guid=MAXDEV_Python_using_pymxs_html",
62 | "44985F87-C175-4F3D-B70F-9FA0B6242AE1")
63 | ]
64 | }
65 |
66 | def get_version_topics(version):
67 | """Get the version-dependent topics, defaulting on the latest
68 | if the requested one does not exist"""
69 | return MAX_VERSION_TOPICS[version if version in MAX_VERSION_TOPICS else 2024]
70 |
71 | V_TOPICS = get_version_topics(MAX_VERSION)
72 |
73 | PYSIDE6_DOC = ("pyside6",
74 | "Qt for Python Documentation (PySide6)",
75 | "doc.qt.io/qtforpython-6/index.html",
76 | "E0E5F945-CD55-404A-840B-81540829E4C4")
77 |
78 | PYSIDE2_DOC = ("pyside2",
79 | "Qt for Python Documentation (PySide2)",
80 | "doc.qt.io/qtforpython-5/contents.html",
81 | "13EEE11E-1BBB-470E-B757-F536D91215A9")
82 |
83 | TOPICS = V_TOPICS + [
84 | ("howtos",
85 | "Python HowTos Github Repo",
86 | "github.com/ADN-DevTech/3dsMax-Python-HowTos",
87 | "2504EEA5-27D6-4EA0-A7A3-B3C058777ADC"),
88 | ("samples",
89 | "Python samples (Github Repo)",
90 | "github.com/ADN-DevTech/3dsMax-Python-HowTos/tree/master/src/samples",
91 | "8ED9D9CC-3799-435D-8016-0F8F16D84004"),
92 | PYSIDE6_DOC if MAX_VERSION >= 2025 else PYSIDE2_DOC,
93 | ("python",
94 | f"Python {PYTHON_VERSION} Documentation",
95 | f"docs.python.org/{PYTHON_VERSION}/",
96 | "B51BCC07-D9E3-439C-AC88-85BD64B97912")
97 | ]
98 |
99 | MENU_LOCATION = ["&Scripting", "Python3 Development", "Browse Documentation"]
100 |
101 | def startup():
102 | """
103 | Hook the function to a menu item.
104 | """
105 | for topic in TOPICS:
106 | menuhook.register(
107 | f"inbrowserhelp_{topic[0]}",
108 | "howtos",
109 | lambda topic=topic: webbrowser.open(f"https://{topic[2]}"),
110 | MENU_LOCATION,
111 | text=topic[1],
112 | tooltip=topic[1],
113 | in2025_menuid=menuhook.BROWSE_DOCUMENTATION,
114 | id_2025=topic[3])
115 |
--------------------------------------------------------------------------------
/src/packages/mxs2py/mxs2py/limitations.py:
--------------------------------------------------------------------------------
1 | """
2 | Define the limitations of the translator.
3 | When something is not fully implemented, or if the translator knows
4 | that it cannot deal with a situation it adds these explanations in the
5 | generated code.
6 | """
7 | L1 = "L1"
8 | L2 = "L2"
9 | L3 = "L3"
10 | L4 = "L4"
11 | L5 = "L5"
12 | L6 = "L6"
13 | L7 = "L7"
14 | L8 = "L8"
15 | L9 = "L9"
16 | L10 = "L10"
17 | L11 = "L11"
18 |
19 | LIMITATIONS = {
20 | L1:
21 | """Passing a parameter by reference to a maxscript function may fail.
22 | in maxscript we can do:
23 | myfunction &a
24 | This will currently be transalated to python as:
25 | myfunciton(pymxs.byref(a))
26 | But should instead be translated as (if a is not yet defined):
27 | a=None
28 | myfunciton(byref(a))
29 | Workaround: add a=None manually before the call if a is not defined
30 | """,
31 |
32 | L2:
33 | """rt.free cannot operate on python strings because they are immutable
34 | in maxscript we can do:
35 | a = "aaa"
36 | b = a
37 | free a
38 | print b -- will display ""
39 | this means that the string itself has been emptied. But python strings
40 | are immutable and pymxs automatically converts maxscript strings
41 | to python strings in most situations (so expect rt.free to never work
42 | from a python program)
43 | Workaround:
44 | in some cases, setting the string to "" may work depending on
45 | the logic of the program
46 | """,
47 | L3:
48 | """
49 | in maxscript you can "collect" the results of a for loop. This means
50 | that each iteration produces a value, and that the list of values is
51 | returned from the loop. This is somewhat similar to a list
52 | comprehension in python but max2py does not support this yet.
53 | Workaround:
54 | Hand translation is needed, so something like this:
55 | for i = 1 to 10 collect i * 3
56 | Needs to be translated to
57 | [i * 3 for i in range(1, 11)]
58 | """,
59 | L4:
60 | """
61 | in maxscript, structs can have event handlers:
62 | struct foo2
63 | (
64 | A = 1,
65 | B = 3,
66 | fn error = throw "ZZZ",
67 | on create do format "Struct Created: %\n" this,
68 | on clone do format "Struct Cloned: %\n" this
69 | )
70 | the translation of the these handlers to python code
71 | is not yet supported.
72 | Workaround:
73 | - in the case of on create, the code could be added to the constructor
74 | - in other cases (clone) translation to python is more difficult
75 | """,
76 | L5:
77 | """
78 | maxscript code may use variable names that are reserved keywords in
79 | python. To avoid syntax errors in the generated python code, the capitalization
80 | of these names is changed. For example yield will be transformed to yIELD.
81 | This will not impact calls into pymxs.runtime because on the maxscript side
82 | things are case insensitive.
83 | """,
84 | L6:
85 | """
86 | (broken, limited support)
87 | macroscripts use special MAXScript syntax (not a library call). They
88 | are supported in python by code that internally generates maxscript
89 | and then evaluates it. This code is in a very early form and does not
90 | work in most situations.
91 | """,
92 | L7:
93 | """
94 | (broken, very limited support)
95 | rollouts use special MAXScript syntax (not a library call). They
96 | are supported in python by code that internally generates maxscript
97 | and then evaluates it. This code is in a very early form and does not
98 | work in most situations.
99 | """,
100 | L8:
101 | """
102 | (broken, very limited support)
103 | plugins use special MAXScript syntax (not a library call). They
104 | are supported in python by code that internally generates maxscript
105 | and then evaluates it. This code is in a very early form and does not
106 | work in most situations.
107 | """,
108 | L9:
109 | """
110 | (broken, very limited support)
111 | attributes use special MAXScript syntax (not a library call). They
112 | are supported in python by code that internally generates maxscript
113 | and then evaluates it. This code is in a very early form and does not
114 | work in most situations.
115 | """,
116 | L10:
117 | """
118 | (broken, very limited support)
119 | tools use special MAXScript syntax (not a library call). They
120 | are supported in python by code that internally generates maxscript
121 | and then evaluates it. This code is in a very early form and does not
122 | work in most situations.
123 | """,
124 | L11:
125 | """
126 | Unexpected construct in the syntax tree. Ignored. Nothing to do for
127 | the user.
128 | """
129 | }
130 |
--------------------------------------------------------------------------------
/src/samples/unicode_io.py:
--------------------------------------------------------------------------------
1 | """
2 | Demonstrate file io using unicode paths and unicode content.
3 | """
4 | import tempfile
5 | import os
6 | import codecs
7 | import shutil
8 |
9 | # Strings for the file content
10 | TEXT_STR = 'Text String: Hello!\n'
11 | UNI_TEXT_STR = 'Unicode String: 女時代'
12 |
13 | # Get the current working folder
14 | CURRENT_DIR = os.getcwd()
15 |
16 | # Create Unicode directory name
17 | UNI_DIR = '時'
18 |
19 | # Set our user folder to the user temp folder
20 | TEMP_DIR = tempfile.gettempdir()
21 |
22 | # Create Unicode file name
23 | UNI_FILE = 'Pÿ x Mxs.txt'
24 |
25 | # Set our temp folder plus the Unicode directory
26 | FULL_PATH = TEMP_DIR + '\\' + UNI_DIR
27 |
28 | # Set our filename
29 | F_NAME = UNI_FILE
30 |
31 | def create_uni_dir():
32 | """Create a directory with a unicode name."""
33 | # Remove directory if it already exists
34 | if os.path.exists(FULL_PATH):
35 | remove_uni_dir()
36 | try:
37 | # Make sure we are in the correct directory root
38 | os.chdir(TEMP_DIR)
39 | print('Working Directory:\n ' + os.getcwd())
40 | except IOError:
41 | print('!FAIL! Could not set working directory!\n')
42 | else:
43 | print('Moved to Temp folder:\n ' + os.getcwd())
44 |
45 | try:
46 | # Make our directory
47 | os.mkdir(FULL_PATH)
48 | except IOError:
49 | print('FAIL! Could not create unicode directory:\n' + FULL_PATH)
50 | else:
51 | print('Created unicode directory:\n' + FULL_PATH)
52 |
53 | def remove_uni_dir():
54 | """Remove a directory with a unicode name."""
55 | # Check if the directory exists
56 | if os.path.exists(FULL_PATH):
57 | try:
58 | # Change to our working folder to be safe
59 | os.chdir(TEMP_DIR)
60 | print('Working Directory:\n ' + os.getcwd())
61 | except IOError:
62 | print('!FAIL! Directory does not exist!\n')
63 | else:
64 | # Since we know we are in our working folder, remove the Unicode
65 | # directory created my createDir()
66 | shutil.rmtree(UNI_DIR)
67 | print('Removed unicode directory:\n' + FULL_PATH)
68 |
69 | def open_file():
70 | """Open a file in working directory and write in it."""
71 | # Change to our working folder to be safe
72 | os.chdir(TEMP_DIR)
73 | # Set up our file and set it's encoding to UTF-8
74 | with codecs.open(F_NAME, encoding='utf-8', mode='w+') as thefile:
75 | # Write to our file (this could be done as a try)
76 | thefile.write(TEXT_STR + UNI_TEXT_STR)
77 | print('Finished writing file to ' + F_NAME)
78 | # Close our file
79 | thefile.close()
80 |
81 | def open_file_in_uni_dir():
82 | """Open a file in unicode directory and write in it."""
83 | # Change to our working folder to be safe
84 | os.chdir(FULL_PATH)
85 | # Set up our file and set it's encoding to UTF-8
86 | with codecs.open(F_NAME, encoding='utf-8', mode='w+') as thefile:
87 | # Write to our file (this could be done as a try)
88 | thefile.write(TEXT_STR + UNI_TEXT_STR)
89 | print('Finished writing file to ' + FULL_PATH + F_NAME)
90 | # Close our file
91 | thefile.close()
92 |
93 | def remove_uni_file():
94 | """Remove a unicode file."""
95 | # Change to our working folder to be safe
96 | os.chdir(TEMP_DIR)
97 | # Check if the file exists
98 | if os.path.exists(TEMP_DIR + F_NAME):
99 | print('File ' + F_NAME + ' exists and will be removed!')
100 | try:
101 | # Remove our file
102 | os.remove(TEMP_DIR + F_NAME)
103 | except IOError:
104 | print('!FAIL! - File not deleted')
105 | else:
106 | print('File Removed.')
107 |
108 | # Create some setup stats for output
109 | try:
110 | STATS = unicode(
111 | 'Setup:\n' +
112 | 'Current directory: ' +
113 | CURRENT_DIR +
114 | '\nOutput filename: ' +
115 | UNI_FILE +
116 | '\nFile contents: ' +
117 | TEXT_STR +
118 | UNI_TEXT_STR)
119 | except NameError:
120 | STATS = (
121 | 'Setup:\n' +
122 | 'Current directory: ' +
123 | CURRENT_DIR +
124 | '\nOutput filename: ' +
125 | UNI_FILE +
126 | '\nFile contents: ' +
127 | TEXT_STR +
128 | UNI_TEXT_STR)
129 | # Output stats
130 | print(STATS)
131 |
132 | # Run our functions
133 | open_file()
134 | create_uni_dir()
135 | open_file_in_uni_dir()
136 | # Comment these out to leave written files and created directory
137 | # to visually verify files and files content
138 | remove_uni_dir()
139 | remove_uni_file()
140 |
--------------------------------------------------------------------------------
/src/packages/mxthread/mxthread/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Provide a way to decorate python functions so that they are always executed in
3 | the main thread of 3dsMax. The functions can throw exceptions and return values
4 | and this is propagated to the caller in the thread.
5 | """
6 | import sys
7 | import os
8 | import functools
9 | from qtpy.QtCore import QObject, Slot, Signal, QThread, QMutex, QWaitCondition, QTimer
10 | from qtpy.QtWidgets import QApplication
11 | #pylint: disable=W0703,R0903
12 |
13 | class RunnableWaitablePayload():
14 | """
15 | Wrap a function call as a payload that will be emitted
16 | to a slot owned by the main thread. The main thread will execute
17 | the function call and package the return value in the payload.
18 | The payload also contains a wait condition that the main thread will
19 | signal when the payload was executed. The worker thread (that creates
20 | the payload) will wait for this wait condition and then retrieve the
21 | return value from the function.
22 | """
23 | def __init__(self, todo):
24 | """
25 | Initialize the payload.
26 | todo is the function to execute, that takes no arguments but
27 | that can return a value.
28 | """
29 | self.todo = todo
30 | self.todo_exception = None
31 | self.todo_return_value = None
32 | self.wcnd = QWaitCondition()
33 | self.mutex = QMutex()
34 |
35 | def wait_for_todo_function_to_complete_on_main_thread(self):
36 | """
37 | Wait for the pending operation to complete
38 | Returns the value returned by the todo function (the function
39 | to execute in this payload).
40 | """
41 | self.mutex.lock()
42 | # queue the thing to do on the main thread
43 | RUNNABLE_PAYLOAD_SIGNAL.sig.emit(self)
44 | # while waiting the QWaitCondition unlocks the mutex
45 | # and relocks it when the wait completes
46 | self.wcnd.wait(self.mutex)
47 | self.mutex.unlock()
48 | # if the payload failed, propagate this to the thread
49 | if self.todo_exception:
50 | raise self.todo_exception
51 | # otherwise return the result
52 | return self.todo_return_value
53 |
54 | def run_todo_function(self):
55 | """
56 | Run the todo function of payload.
57 | This will add the return of the todo function to the payload as "todo_return_value".
58 | """
59 | self.mutex.lock()
60 | try:
61 | self.todo_return_value = self.todo()
62 | except Exception as exception:
63 | self.todo_exception = exception
64 | self.wcnd.wakeAll()
65 | self.mutex.unlock()
66 |
67 | class RunnableWaitablePayloadSignal(QObject):
68 | """
69 | Creates a signal that can be used to send RunnableWaitablePyaloads to
70 | the main thread for execution.
71 | """
72 | sig = Signal(RunnableWaitablePayload)
73 |
74 | # Create the Slots that will receive signals
75 | class PayloadSlot(QObject):
76 | """
77 | Slot for function submission on the main thread.
78 | """
79 | def __init__(self):
80 | """
81 | An object that owns a slot.
82 | This object's affinity is the main thread so that signals it receives
83 | will run on the main thread.
84 | """
85 | QObject.__init__(self)
86 | self.moveToThread(QApplication.instance().thread())
87 |
88 | @Slot(RunnableWaitablePayload)
89 | def run(self, ttd):
90 | """
91 | Run the slot payload.
92 | """
93 | ttd.run_todo_function()
94 |
95 | RUNNABLE_PAYLOAD_SLOT = PayloadSlot()
96 |
97 | # connect the payload signal to the payload slot
98 | RUNNABLE_PAYLOAD_SIGNAL = RunnableWaitablePayloadSignal()
99 | RUNNABLE_PAYLOAD_SIGNAL.sig.connect(RUNNABLE_PAYLOAD_SLOT.run)
100 |
101 | def run_on_main_thread(todo, *args, **kwargs):
102 | """
103 | Run code on the main thread.
104 | Returns the return value of the todo code. If this is called
105 | from the main thread, todo is immediately called.
106 | """
107 | if QThread.currentThread() is QApplication.instance().thread():
108 | return todo(*args, **kwargs)
109 | ttd = RunnableWaitablePayload(lambda: todo(*args, **kwargs))
110 | return ttd.wait_for_todo_function_to_complete_on_main_thread()
111 |
112 | def on_main_thread(func):
113 | """
114 | Decorate a function to make it always run on the main thread.
115 | """
116 | # preserve docstring of the wrapped function
117 | @functools.wraps(func)
118 | def decorated(*args, **kwargs):
119 | return run_on_main_thread(func, *args, **kwargs)
120 | return decorated
121 |
122 | @on_main_thread
123 | def main_thread_print(*args, **kwargs):
124 | """
125 | Print on the main thread.
126 | """
127 | return print(*args, **kwargs)
128 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 3ds Max Python How Tos
2 | ### Practical Python 3 Development Examples For 3ds Max
3 |
4 | 
5 |
6 | This repo contains various Python programming examples and tutorials targeting [3ds Max](https://www.autodesk.ca/en/products/3ds-max/overview).
7 |
8 | All the examples in the tutorials are implemented as pip packages. This is a bit heavy for
9 | small things (we provide a setup.py, a LICENSE and everything) but makes things installable
10 | and shareable more easily. As soon as something has dependencies on external packages or requires
11 | more than one Python file, pip packages become very convenient. Because we think it is a good
12 | practice to package 3ds Max Python tools with pip, we provide all our examples in this form.
13 |
14 | ## Installation
15 |
16 | It is not necessary to install the HowTos: the repo can simply be used as a passive
17 | directory of samples and documentation for Python developers.
18 |
19 | - Installing the HowTos will add menu items to 3ds Max, and is documented [here](doc/install.md)
20 | - After an update from github it is necessary to rerun install scripts to get everything
21 | working as expected
22 |
23 |
24 | ## Python How Tos
25 |
26 | ### New content
27 |
28 | - New with 3dsMax 2025: [Plugin Packages in 2025 and Integration With the New Menu System](/doc/pluginpackage.md)
29 |
30 | ### Samples
31 |
32 | The samples below are translations of [MAXScript How Tos](https://help.autodesk.com/view/MAXDEV/2022/ENU/?guid=GUID-25C9AD58-3665-471E-8B4B-54A094C1D5C9) that
33 | can be found in the 3ds Max online documentation.
34 |
35 | The conversion from MaxScript to Python could have been more mechanical but we chose to implement
36 | the Python version in the best Python way known to us. An example of this is that we use PySide
37 | (Qt) for the UI as much as possible instead of using more traditional 3ds Max ui mechanisms.
38 |
39 | *How To?*
40 |
41 | - Develop a Transform Lock Script [transformlock](/src/packages/transformlock/README.md)
42 | - Remove all materials [removeallmaterials](/src/packages/removeallmaterials/README.md)
43 | - Quickly rename selected objects [renameselected](/src/packages/renameselected/README.md)
44 | - Output Object Data to File [speedsheet](/src/packages/speedsheet/README.md)
45 | - Create a quick video preview [quickpreview](/src/packages/quickpreview/README.md)
46 | - Access the Z-Depth Channel [zdepthchannel](/src/packages/zdepthchannel/README.md)
47 |
48 | ## Python Examples that don't come from maxscript howtos
49 |
50 | - Update a progressbar from a Python thread [threadprogressbar](/src/packages/threadprogressbar/README.md)
51 | - Create a single instance modal dialog [singleinstancedlg](/src/packages/singleinstancedlg/README.md)
52 | - Add menu items to open documentation pages in the web browser [inbrowserhelp](/src/packages/inbrowserhelp/README.md)
53 | - Integrate a Python Console [pyconsole](/src/packages/pyconsole/README.md)
54 | - Run code on thre main thread [mxthread](/src/packages/mxthread/README.md)
55 | - Automatically convert maxscript to python [mxs2py](/src/packages/mxs2py/README.md)
56 | - Use socketio from 3dsMax [socketioclient](/src/packages/socketioclient/README.md)
57 | - Drop maxscript code on a rich text window to get a python translation [mxstranslate](/src/packages/mxstranslate/README.md)
58 |
59 | ## Python Samples
60 |
61 | Python samples can be found in [src/samples](/src/samples). These samples may already be in your 3ds Max
62 | installation directories.
63 |
64 | ## 3dsMax startup entry point
65 |
66 | [pystartup](/src/pystartup/README.md) provides the maxscript code that, when copied to 3ds Max's
67 | startup directory, will automatically launch pip packages with the 3dsMax startup
68 | entry point.
69 |
70 | ## Tools
71 |
72 | The following packages are not really examples but Python tools.
73 |
74 | - [menuhook](/src/packages/menuhook/README.md) is not meant to be an example (but is still interesting as such!) but
75 | as a way of attaching Python functions to 3ds Max menu items. The menuhook package is used by
76 | most of the other samples.
77 |
78 | - [realoadmod](/src/packages/reloadmod/README.md) is small tool that will reload all development modules in one
79 | operation
80 |
81 | - [mxvscode](/src/packages/mxvscode/README.md) is a small tool that will automatically import debugpy (the
82 | VSCode debugging interface) during the startup of 3ds Max and make it accept remote connections.
83 | This may slow down the startup of 3ds Max quite a bit and is meant as a developer-only tool.
84 |
85 | ## Extra Goodies
86 |
87 | - [install.sh](install.sh) will install pip, install pystartup and pip install all the samples
88 | - [uninstall.sh](uninstall.sh) will uninstall what was installed with install.sh
89 | - [installstartup.sh](installstartup.sh) will install pip and pystartup and nothing more
90 | - [installhowtos.sh](installhowtos.sh) will install only the howtos (works in a virtual env)
91 | - [checks.sh](/scripts/checks.sh) runs pylint on the code, validates that 3ds Max is named properly,
92 | validates that code blocks in markdown always specify the programming language, checks that
93 | all links are valid in all markdown files of the repo
94 | - [create.sh](/scripts/create.sh) will generate an empty pip package in the current working directory.
95 |
--------------------------------------------------------------------------------
/src/packages/mxs2py/mxs2py/syntax.py:
--------------------------------------------------------------------------------
1 | """
2 | Definition of the various syntax constructs in the syntax tree.
3 | """
4 | ARRAY = "ARRAY"
5 | ASSIGNMENT = "ASSIGNMENT"
6 | BITARRAY = "BITARRAY"
7 | BITARRAY_RANGE = "BITARRAY_RANGE"
8 | CALL = "CALL"
9 | CASE_EXPR = "CASE_EXPR"
10 | CASE_ITEM = "CASE_ITEM"
11 | COMPUTATION = "COMPUTATION"
12 | CONTEXT_ABOUT = "CONTEXT_ABOUT"
13 | CONTEXT_AT = "CONTEXT_AT"
14 | CONTEXT_EXPR = "CONTEXT_EXPR"
15 | CONTEXT_IN_COORDSYS = "CONTEXT_IN_COORDSYS"
16 | CONTEXT_IN_NODE = "CONTEXT_IN_NODE"
17 | CONTEXT_WITH = "CONTEXT_WITH"
18 | DO_LOOP = "DO_LOOP"
19 | DECL = "DECL"
20 | FOR_LOOP = "FOR_LOOP"
21 | FOR_LOOP_FROM_TO_SEQUENCE = "FOR_LOOP_FROM_TO_SEQUENCE"
22 | FUNCTION_DEF = "FUNCTION_DEF"
23 | FUNCTION_RETURN = "FUNCTION_RETURN"
24 | GLOBAL = "GLOBAL"
25 | VAR_NAME = "VAR_NAME"
26 | IF_EXPR = "IF_EXPR"
27 | LOCAL = "LOCAL"
28 | LOCAL_DECL = "LOCAL_DECL"
29 | GLOBAL_DECL = "GLOBAL_DECL"
30 | LOOP_CONTINUE = "LOOP_CONTINUE"
31 | LOOP_EXIT = "LOOP_EXIT"
32 | MAX_COMMAND = "MAX_COMMAND"
33 | MOUSETOOL_DEF = "MOUSETOOL_DEF"
34 | NAME = "NAME"
35 | ARGUMENT = "ARGUMENT"
36 | NAMED_ARGUMENT = "NAMED_ARGUMENT"
37 | NUMBER = "NUMBER"
38 | OPERATOR = "OPERATOR"
39 | PATH_NAME = "PATH_NAME"
40 | PARAMETERS_DEF = "PARAMETERS_DEF"
41 | PARAMETERS_HANDLER = "PARAMETERS_HANDLER"
42 | PERSISTENTGLOBAL = "PERSISTENTGLOBAL"
43 | ATTRIBUTES_DEF = "ATTRIBUTES_DEF"
44 | POINT2 = "POINT2"
45 | POINT3 = "POINT3"
46 | POINT4 = "POINT4"
47 | PROGRAM = "PROGRAM"
48 | PROPERTY = "PROPERTY"
49 | PROPERTY_ACCESSOR_INDEX = "PROPERTY_ACCESSOR_INDEX"
50 | PROPERTY_ACCESSOR_MEMBER = "PROPERTY_ACCESSOR_MEMBER"
51 | QUESTION = "QUESTION"
52 | RCMENU_ITEM = "RCMENU_ITEM"
53 | RCMENU_HANDLER = "RCMENU_HANDLER"
54 | RCMENU_DEF = "RCMENU_DEF"
55 | REFERENCE = "REFERENCE"
56 | ROLLOUT_CLAUSE = "ROLLOUT_CLAUSE"
57 | ROLLOUT_DEF = "ROLLOUT_DEF"
58 | ROLLOUT_GROUP = "ROLLOUT_GROUP"
59 | ROLLOUT_HANDLER = "ROLLOUT_HANDLER"
60 | ROLLOUT_ITEM = "ROLLOUT_ITEM"
61 | SET_CONTEXT = "SET_CONTEXT"
62 | SINGLECOMMENT = "SINGLECOMMENT"
63 | STRING = "STRING"
64 | STRUCT_DEF = "STRUCT_DEF"
65 | STRUCT_MEMBER_ASSIGN = "STRUCT_MEMBER_ASSIGN"
66 | STRUCT_MEMBER_DATA = "STRUCT_MEMBER_DATA"
67 | STRUCT_MEMBER_METHOD = "STRUCT_MEMBER_METHOD"
68 | EXPR_SEQ = "EXPR_SEQ"
69 | THROW = "THROW"
70 | TIME = "TIME"
71 | SMPTE_TIME = "SMPTE_TIME"
72 | TRY_EXPR = "TRY_EXPR"
73 | UNARY_MINUS = "UNARY_MINUS"
74 | UNARY_NOT = "UNARY_NOT"
75 | VARIABLE_DECL = "VARIABLE_DECL"
76 | WHEN_ATTRIBUTE = "WHEN_ATTRIBUTE"
77 | WHEN_OBJECTS = "WHEN_OBJECTS"
78 | WHILE_LOOP = "WHILE_LOOP"
79 | MACROSCRIPT_DEF = "MACROSCRIPT_DEF"
80 | ON_DO_HANDLER = "ON_DO_HANDLER"
81 | ON_CLONE_DO_HANDLER = "ON_CLONE_DO_HANDLER"
82 | ON_MAP_DO_HANDLER = "ON_MAP_DO_HANDLER"
83 | MACROSCRIPT_CLAUSE = "MACROSCRIPT_CLAUSE"
84 | PLUGIN_DEF = "PLUGIN_DEF"
85 | UTILITY_DEF = "UTILITY_DEF"
86 |
87 | # the following syntax appears as a result of transforming
88 | # the mxs tree. So this syntax does not come from mxs but instead
89 | # is python syntax generated during tree transformation
90 | PY_SHIM_VAR_NAME = "PY_SHIM_VAR_NAME"
91 | PY_BUILTIN_VAR_NAME = "PY_BUILTIN_VAR_NAME"
92 | PY_RT_VAR_NAME = "PY_RT_VAR_NAME"
93 | PY_NONLOCAL = "PY_NONLOCAL"
94 | PY_GLOBAL = "PY_GLOBAL"
95 | PY_MACROSCRIPT_CLASS = "PY_MACROSCRIPT_CLASS"
96 | PY_ROLLOUT_CLASS = "PY_ROLLOUT_CLASS"
97 | PY_PLUGIN_CLASS = "PY_PLUGIN_CLASS"
98 | PY_ATTRIBUTES_CLASS = "PY_ATTRIBUTES_CLASS"
99 | PY_TUPLE = "PY_TUPLE"
100 | PY_NOVAR = "PY_NOVAR"
101 |
102 | # levels of comments
103 | COMMENT_WARNING = "WARNING"
104 | COMMENT_ERROR = "ERROR"
105 | COMMENT_INFO = "INFO"
106 |
107 |
108 | class Construct:
109 | """Syntactical construct"""
110 | def __init__(self, construct, *args):
111 | self.construct = construct
112 | self.args = list(args)
113 | self.start = None
114 | self.end = None
115 | self.comments = []
116 |
117 | def set_start_end(self, start, end):
118 | """Set delimitation position of the construct"""
119 | self.start = start
120 | self.end = end
121 |
122 | # pylint: disable=unused-argument
123 | def warning_comment(self, new_comment, addendum=None):
124 | """Add a warning comment"""
125 | self.comments.append((COMMENT_WARNING, new_comment))
126 | def error_comment(self, new_comment, addendum=None):
127 | """Add an error comment"""
128 | self.comments.append((COMMENT_ERROR, new_comment))
129 | def info_comment(self, new_comment, addendum=None):
130 | """Add an info comment"""
131 | self.comments.append((COMMENT_INFO, new_comment))
132 |
133 | def __str__(self):
134 | """Convert to a string"""
135 | tab = "."
136 | def indent(lines):
137 | return "\n".join(map(lambda toindent: tab + toindent, lines.split("\n")))
138 |
139 | def stritem(i):
140 | if isinstance(i, list):
141 | return "(list)\n" + indent("\n".join(map(stritem, i)))
142 | if isinstance(i, tuple):
143 | return "(tuple)\n" + indent("\n".join(map(stritem, list(i))))
144 | return str(i)
145 |
146 | construct = (f"{self.construct}\n"
147 | if self.start is None
148 | else f"{self.construct} {self.start} {self.end}\n")
149 | return (
150 | construct +
151 | indent("\n".join(
152 | map(
153 | stritem,
154 | self.args)
155 | ))
156 | )
157 |
--------------------------------------------------------------------------------
/src/packages/zdepthchannel/README.md:
--------------------------------------------------------------------------------
1 | # HowTo: zdepthchannel
2 |
3 | 
4 |
5 | [Original MaxScript Tutorial](https://help.autodesk.com/view/MAXDEV/2022/ENU/?guid=GUID-3667A33C-E3E4-4F39-A480-3713240838F1)
6 | [Source Code](zdepthchannel/__init__.py)
7 |
8 | The 3ds Max default scanline renderer generates a multitude of additional data
9 | channels providing information about colors, texture coordinates, normals,
10 | transparency, velocity, coverage etc. All of these channels can be accessed for
11 | reading via MAXScript.
12 |
13 | The following simple script will render an image including Z-buffer depth info
14 | and convert it into what is known as a "voxel landscape" using the depth data
15 | to generate geometry representing the single image pixels.
16 |
17 | *Goals:*
18 | - learn to access the z-buffer
19 |
20 | ## Explanations
21 |
22 | - Delete all objects from the last session.
23 | - Render the current view with Z-buffer info to a bitmap
24 | - Go through all lines in the rendered image and through all pixels in each line
25 | - For every pixel, get the color and the z-depth value and create a box representing that pixel using the color for the object’s wireframe color and the z-depth for the box’s height.
26 | - Give each object a unique name to be able to easily delete them when needed.
27 | - Display a progress bar to show how long it will take to go through all pixels.
28 |
29 | ## Using the tool
30 |
31 | From the 3ds Max listener window we can do:
32 |
33 | ```python
34 | import zdepthchannel
35 |
36 | zdepthchannel.startup()
37 | ```
38 |
39 | If we install this sample as a pip package it will be automatically
40 | started during the startup of 3ds Max (because it defines a startup
41 | entry point for 3ds Max).
42 |
43 | ## Understanding the code
44 |
45 | Everything is done in the zdepthchannel function in [zdepthchannel/\_\_init\_\_.py](zdepthchannel/__init__.py).
46 |
47 | We first look for objects starting by "VoxelBox". These would be the result
48 | of previously running the code on the same scene.
49 |
50 | Because the MAXScript `$` notation is not available in Python we use
51 | a regex to filter `rt.objects` that match our pattern:
52 |
53 | ```python
54 | voxelbox = re.compile("^VoxelBox")
55 | for tbd in filter(lambda o: voxelbox.match(o.name), list(rt.objects)):
56 | rt.delete(tbd)
57 | ```
58 |
59 | Now we render the active viewport at 32x32 resolution and store the result in
60 | the variable rbmp . We request an additional Z-depth channel to be generated.
61 | The Virtual Frame Buffer will be disabled during rendering.
62 |
63 | ```python
64 | rbmp = rt.render(outputsize=rt.Point2(32, 32), channels=[zdepth_name], vfb=False)
65 | ```
66 |
67 | After rendering the image, we request a copy of the ZDepth channel as a
68 | greyscale mask. It will be stored in the bitma z\_d .
69 |
70 | ```python
71 | z_d = rt.getChannelAsMask(rbmp, zdepth_name)
72 | ```
73 |
74 | We start a new progress indicator with a respective caption - it will be
75 | displayed in the status bar at the bottom of the UI.
76 |
77 | ```python
78 | rt.progressStart("Rendering Voxels...")
79 | ```
80 |
81 | Now we start looping through all lines in the rendered bitmap. The variable y
82 | will change from 1 to the height of the bitmap.
83 |
84 | ```python
85 | for y in range(1, rbmp.height):
86 | print("y =", y)
87 | ```
88 |
89 | The progressupdate method requires a percentage value. We divide the current
90 | bitmap line number y by the total number of lines rbmp.height . This yields a
91 | result between 0.0 and 1.0, multiplied by 100.0 it returns a percentage between
92 | 0.0 and 100.0. To react to the Cancel button, we exit the loop if
93 | progressUpdate returns a False value.
94 |
95 | ```python
96 | if not rt.progressupdate(100.0 * y / rbmp.height):
97 | break
98 | ```
99 |
100 | Now we read a whole line of pixels from both the RGBA bitmap and its z-buffer.
101 | Note that bitmap indices are 0-based and count from 0 to height minus one. This
102 | explains the "minus one" in the vertical position value.
103 |
104 | ```python
105 | pixel_line = rt.getPixels(rbmp, rt.Point2(0, y-1), rbmp.width)
106 | z_line = rt.getPixels(z_d, rt.Point2(0, y-1), rbmp.width)
107 | ```
108 |
109 | By counting from 1 to the width of the bitmap, we access each pixel of the
110 | current scanline.
111 |
112 | ```python
113 | for x in range(1, rbmp.width):
114 | print("x =", x, z_line[x].value)
115 | ```
116 |
117 | Now we create a box with width and length equal to 10, and height equal to half
118 | the Z-buffer's grayscale value. We also set the X and Y position to every 10 th
119 | world unit based on the pixel's coordinates. Note that we have to use the
120 | negative y value to get the upper left corner appear up left. A bitmap starts
121 | up left, while the positive quadrant of the MAX world space starts down left.
122 | This way we mirror the vertical coordinate to get a valid representation of the
123 | bitmap in the viewport.
124 |
125 | ```python
126 | box = rt.box(width=10, length=10, height=(z_line[x].value/2))
127 | box.pos = rt.Point3(x*10, -y*10, 0)
128 | ```
129 |
130 | Using the RGB color of the rendered pixel, we assign a new wireframe color to
131 | the new box.
132 |
133 | ```python
134 | box.wirecolor = pixel_line[x]
135 | ```
136 |
137 | We also set its box's name to a unique name with the base "VoxelBox"
138 |
139 | ```python
140 | box.name = rt.uniqueName("VoxelBox")
141 | ```
142 |
143 | After the loops are over, we can end the progress display.
144 |
145 | ```python
146 | rt.progressEnd()
147 | ```
148 |
--------------------------------------------------------------------------------
/src/packages/menuhook/README.md:
--------------------------------------------------------------------------------
1 | # menuhook
2 |
3 | This package provides a convenient way to hook a Python function
4 | to a 3ds Max menu item.
5 |
6 | ## Example
7 |
8 | When a component is loaded, it can add itself to a 3ds Max menu by doing this:
9 |
10 | ```python
11 | import menuhook
12 |
13 | def fcn():
14 | """
15 | Do something. This is the function we want to attach to a menu
16 | item.
17 | """
18 | print("hello")
19 |
20 | menuhook.register(
21 | # the name of the action (will be listed in menu customization dialogs)
22 | "myaction",
23 | # the category for the action
24 | "my category",
25 | # the function to be executed (this is registered for a single run of 3ds Max)
26 | fcn,
27 | # the menu for this action. if None is provided the action
28 | # will not be added to a menu, only created as an action.
29 | # this can be deeply nested.
30 | menu=["&Scripting", "Samples"],
31 | # the menu text. if None is provided, the action name will be used
32 | text"the text for the menu item",
33 | # the menu tool tip. if None is provided, the action name will be used
34 | tooltip="the tooltip")
35 | ```
36 |
37 | - The menu item will only be added once for this (action, category) in
38 | a given 3ds Max installation (if the action is already present, the menu item
39 | will not be created)
40 |
41 | - If during the startup of 3ds Max the Python module is not loaded (but was
42 | loaded in a previous run), the menu item will still be there. When
43 | invoking it the user will be notified that a Python module required to
44 | implement the function was not loaded.
45 |
46 | - The user remains the owner of menus (as it is in the current 3ds Max
47 | implementation). Yes a new component may add an item once to a menu, but
48 | the user is free to remove it, move it, duplicate it and his choices will
49 | never be overridden.
50 |
51 | ## Q & A
52 |
53 | *Q:* Why not using PySide directly?
54 |
55 | *A:* 3ds Max uses Qt for its menu and technically they can be inspected
56 | and modified during PySide. But the Menu Manager inside 3ds Max owns the
57 | menus and can regenerate them (using Qt) at any time during the execution
58 | of 3ds Max. And because of that any change made to the menus using PySide
59 | instead of the 3ds Max Menu Manager will be lost. In short: things will
60 | not behave as expected.
61 |
62 |
63 | *Q:* My Python component is in a virtual env that I only use for launching
64 | 3ds Max for special tasks (e.g. I have a special configuration of 3ds Max for
65 | software development that includes tools that are not used for content
66 | creation... I do this to reduce the clutter for my users) but I still see
67 | its menu items when launching 3ds Max outside this virtual environment. Worse,
68 | when I do this the menu items don't work but display a message that something
69 | is missing.
70 |
71 | *A:* There is no solution for this at this point. Menu items, once created,
72 | remain in the menu structure forever and are owned by the user of 3ds Max
73 | who can move them around, duplicate them, remove them, etc. This may
74 | change in the future.
75 |
76 |
77 | *Q:* Doing this creates a 'macro' and this macro never goes away
78 |
79 | *A:* The Python & maxscript apis currently do not allow to remove macros
80 | that have been created. So the [macros](https://help.autodesk.com/view/MAXDEV/2022/ENU/?guid=GUID-3DC75DDE-E4BC-4033-ABA9-A42063036CB9)
81 | that we create are permanent
82 | but if we don't load the Python packages that implement them during
83 | an execution of 3ds Max they become dangling. We are able to detect that
84 | and notify the users.
85 |
86 |
87 | *Q:* Would it be possible to remove menu items (to make them not permanent)?
88 |
89 | *A:* The [menu manager](https://help.autodesk.com/view/MAXDEV/2022/ENU/?guid=GUID-258F6015-6B45-4A87-A7F5-BB091A2AE065),
90 | provides a way to remove menu items. This could be used. But the solution
91 | would not be crash proof (it would be difficult to establish that the menu
92 | item has been removed for real, by using only what the menu manager provides).
93 | For now, this will not be supported here.
94 |
95 | ## How the code works
96 |
97 | This is not a sample (but a utility that is used by other samples).
98 |
99 | Nevertheless the code is in [menuhook/__init__.py](menuhook/__init__.py) and does:
100 |
101 | ```python
102 | from pymxs import runtime as rt
103 | ```
104 |
105 | To get access to the 3dsMax scripting library for Python (pymxs).
106 |
107 | Then it uses `rt.macros` to access functions from the [macro scripts](https://help.autodesk.com/view/MAXDEV/2022/ENU/?guid=GUID-3DC75DDE-E4BC-4033-ABA9-A42063036CB9).
108 | As it is now we have to create a macro for a function we want to hook to the menu.
109 |
110 | The macro has an action and a category (that identifies it). It is done by this call:
111 |
112 | ```python
113 | rt.macros.new(
114 | category,
115 | action,
116 | tooltip,
117 | text,
118 | mxs)
119 | ```
120 |
121 | When this (category, action) exists, we can use `rt.menuman`, the [menu manager](https://help.autodesk.com/view/MAXDEV/2022/ENU/?guid=GUID-258F6015-6B45-4A87-A7F5-BB091A2AE065)
122 | to find the menu in which we want to add an item:
123 |
124 | ```python
125 | targetmenu = rt.menuman.findmenu(menu)
126 | ```
127 |
128 | If we manage to find it, we create an action item for our (action, category) and add it to the
129 | target menu. Finallly we update the menubar:
130 |
131 | ```python
132 | if targetmenu:
133 | newaction = rt.menuman.createActionItem(action, category)
134 | if newaction:
135 | targetmenu.addItem(newaction, -1)
136 | rt.menuman.updateMenuBar()
137 | ```
138 |
139 |
--------------------------------------------------------------------------------