├── source ├── test │ ├── __init__.py │ ├── res │ │ └── settings.ini │ ├── examples │ │ └── PlaneProjector_tests.c4d │ ├── objects │ │ ├── object_iterator_tests.c4d │ │ ├── object_hierarchy_tests.c4d │ │ ├── center_object_axis_tests.c4d │ │ ├── object_hierarchy_tests.py │ │ ├── center_object_axis_tests.py │ │ └── object_iterator_tests.py │ ├── maths │ │ ├── matrix_to_listlist_tests.c4d │ │ └── matrix_to_listlist_tests.py │ ├── utils_tests.py │ ├── plugins_tests.py │ ├── maths_tests.py │ └── objects_tests.py └── py4dlib │ ├── examples │ ├── __init__.py │ ├── Extract Polys 1.0 │ │ ├── res │ │ │ ├── icon.tif │ │ │ ├── strings_us │ │ │ │ └── dialogs │ │ │ │ │ └── IDD_DIALOG_SETTINGS.str │ │ │ ├── c4d_symbols.h │ │ │ └── dialogs │ │ │ │ └── IDD_DIALOG_SETTINGS.res │ │ └── Extract.pyp │ ├── Regex Renamer 1.1 │ │ └── res │ │ │ ├── icon.png │ │ │ ├── icons │ │ │ ├── alternative icon 01.tif │ │ │ ├── alternative icon 02.tif │ │ │ └── alternative icon 03.png │ │ │ ├── tutorial.html │ │ │ ├── strings_us │ │ │ └── dialogs │ │ │ │ └── IDD_DIALOG_SETTINGS.str │ │ │ ├── c4d_symbols.h │ │ │ └── dialogs │ │ │ └── IDD_DIALOG_SETTINGS.res │ ├── Plane Projector 1.2 │ │ ├── res │ │ │ ├── icon.png │ │ │ ├── strings_us │ │ │ │ └── dialogs │ │ │ │ │ └── IDD_DIALOG_SETTINGS.str │ │ │ ├── c4d_symbols.h │ │ │ └── dialogs │ │ │ │ └── IDD_DIALOG_SETTINGS.res │ │ └── icons │ │ │ ├── alternative icon 1.png │ │ │ ├── alternative icon 2.png │ │ │ ├── alternative icon 3.png │ │ │ ├── alternative icon 4.png │ │ │ └── alternative icon 5.png │ ├── PrintObjectHierarchy.py │ ├── CreatePlanesFromPolygons.py │ └── ShowPolygonNumber.py │ ├── __init__.py │ ├── plugins.py │ └── utils.py ├── docs ├── img │ └── ShowPolygonNumber.png ├── pydocs3theme │ ├── theme.conf │ └── static │ │ ├── pydoctheme.css_t │ │ ├── pygments.css_t │ │ ├── default.css_t │ │ └── basic.css_t ├── acknowledgements.rst ├── contributions.rst ├── license.rst ├── installation.rst ├── index.rst ├── examples.rst ├── api.rst ├── api │ ├── plugins.rst │ ├── mesh.rst │ ├── utils.rst │ ├── maths.rst │ └── objects.rst ├── usage.rst ├── Makefile ├── make.bat └── conf.py ├── .pydevproject ├── .gitignore └── README.md /source/test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/py4dlib/examples/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/test/res/settings.ini: -------------------------------------------------------------------------------- 1 | [Settings] 2 | string = some string 3 | int = 1 4 | float = 2.0 5 | 6 | -------------------------------------------------------------------------------- /docs/img/ShowPolygonNumber.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreberg/py4dlib/HEAD/docs/img/ShowPolygonNumber.png -------------------------------------------------------------------------------- /docs/pydocs3theme/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = default 3 | stylesheet = pydoctheme.css 4 | pygments_style = sphinx -------------------------------------------------------------------------------- /source/test/examples/PlaneProjector_tests.c4d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreberg/py4dlib/HEAD/source/test/examples/PlaneProjector_tests.c4d -------------------------------------------------------------------------------- /source/test/objects/object_iterator_tests.c4d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreberg/py4dlib/HEAD/source/test/objects/object_iterator_tests.c4d -------------------------------------------------------------------------------- /source/test/maths/matrix_to_listlist_tests.c4d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreberg/py4dlib/HEAD/source/test/maths/matrix_to_listlist_tests.c4d -------------------------------------------------------------------------------- /source/test/objects/object_hierarchy_tests.c4d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreberg/py4dlib/HEAD/source/test/objects/object_hierarchy_tests.c4d -------------------------------------------------------------------------------- /source/test/objects/center_object_axis_tests.c4d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreberg/py4dlib/HEAD/source/test/objects/center_object_axis_tests.c4d -------------------------------------------------------------------------------- /source/py4dlib/examples/Extract Polys 1.0/res/icon.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreberg/py4dlib/HEAD/source/py4dlib/examples/Extract Polys 1.0/res/icon.tif -------------------------------------------------------------------------------- /source/py4dlib/examples/Regex Renamer 1.1/res/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreberg/py4dlib/HEAD/source/py4dlib/examples/Regex Renamer 1.1/res/icon.png -------------------------------------------------------------------------------- /source/py4dlib/examples/Plane Projector 1.2/res/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreberg/py4dlib/HEAD/source/py4dlib/examples/Plane Projector 1.2/res/icon.png -------------------------------------------------------------------------------- /source/py4dlib/examples/Plane Projector 1.2/icons/alternative icon 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreberg/py4dlib/HEAD/source/py4dlib/examples/Plane Projector 1.2/icons/alternative icon 1.png -------------------------------------------------------------------------------- /source/py4dlib/examples/Plane Projector 1.2/icons/alternative icon 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreberg/py4dlib/HEAD/source/py4dlib/examples/Plane Projector 1.2/icons/alternative icon 2.png -------------------------------------------------------------------------------- /source/py4dlib/examples/Plane Projector 1.2/icons/alternative icon 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreberg/py4dlib/HEAD/source/py4dlib/examples/Plane Projector 1.2/icons/alternative icon 3.png -------------------------------------------------------------------------------- /source/py4dlib/examples/Plane Projector 1.2/icons/alternative icon 4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreberg/py4dlib/HEAD/source/py4dlib/examples/Plane Projector 1.2/icons/alternative icon 4.png -------------------------------------------------------------------------------- /source/py4dlib/examples/Plane Projector 1.2/icons/alternative icon 5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreberg/py4dlib/HEAD/source/py4dlib/examples/Plane Projector 1.2/icons/alternative icon 5.png -------------------------------------------------------------------------------- /source/py4dlib/examples/Regex Renamer 1.1/res/icons/alternative icon 01.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreberg/py4dlib/HEAD/source/py4dlib/examples/Regex Renamer 1.1/res/icons/alternative icon 01.tif -------------------------------------------------------------------------------- /source/py4dlib/examples/Regex Renamer 1.1/res/icons/alternative icon 02.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreberg/py4dlib/HEAD/source/py4dlib/examples/Regex Renamer 1.1/res/icons/alternative icon 02.tif -------------------------------------------------------------------------------- /source/py4dlib/examples/Regex Renamer 1.1/res/icons/alternative icon 03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreberg/py4dlib/HEAD/source/py4dlib/examples/Regex Renamer 1.1/res/icons/alternative icon 03.png -------------------------------------------------------------------------------- /docs/acknowledgements.rst: -------------------------------------------------------------------------------- 1 | Acknowledgements 2 | ---------------- 3 | 4 | a.k.a. **The Hall of Fame** 5 | 6 | In no particular order. 7 | 8 | * Lennart Wåhlin (TCA Studios) 9 | * Per-Anders Edwards 10 | * Adam Swaab 11 | * NitroMan (Nitro4D) 12 | * Robert Leger 13 | * Niklas Rosenstein 14 | * Scott Ayers 15 | 16 | Thanks for making some of your C.O.F.F.E.E. and Python scripts 17 | free and for often not denying the ability to take a look. 18 | -------------------------------------------------------------------------------- /docs/contributions.rst: -------------------------------------------------------------------------------- 1 | Contributions 2 | ------------- 3 | 4 | I'd be very happy to accept code contributions. If you'd like to contribute 5 | please fork this repository, commit your changes to your local fork and then 6 | send me a pull request. 7 | 8 | If that's too much of a hassle, you can also go ahead and create a gist or 9 | a pastebin post (or whatever copypasta service you fancy) and send me the URL 10 | so that I can include the code manually. 11 | -------------------------------------------------------------------------------- /.pydevproject: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Default 6 | python 2.7 7 | 8 | /py4dlib/source 9 | 10 | 11 | -------------------------------------------------------------------------------- /source/py4dlib/examples/Regex Renamer 1.1/res/tutorial.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | Re-directing to www.regular-expressions.info 7 | 8 | 9 | 10 | 11 |

Re-directing to www.regular-expressions.info (online connection needed)

12 | 13 | 14 | -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | License 2 | ------- 3 | 4 | .. code:: 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ------------ 3 | 4 | Take the `py4dlib` folder which you can find under `/source` in 5 | this repository, and put it under the following path:: 6 | 7 | /library/python/packages/ 8 | 9 | `` is the prefs directory for CINEMA 4D in your home 10 | directory and `` is the name of the operating system you are 11 | running. 12 | 13 | For example on OS X this path could be:: 14 | 15 | /Users//Library/Preferences/MAXON/CINEMA 4D R/library/python/packages/osx 16 | 17 | On Windows this path could be:: 18 | 19 | %APPDATA%\MAXON\CINEMA 4D R\library\python\packages\win64 20 | -------------------------------------------------------------------------------- /source/py4dlib/examples/Extract Polys 1.0/res/strings_us/dialogs/IDD_DIALOG_SETTINGS.str: -------------------------------------------------------------------------------- 1 | // C4D-DialogResource 2 | 3 | DIALOGSTRINGS IDD_DIALOG_SETTINGS 4 | { 5 | IDS_BUTTON_CANCEL "Cancel"; 6 | IDS_BUTTON_DOIT "Do It!"; 7 | IDS_CHECK "Optimize Unused Points"; 8 | IDS_CHECK1 "Hide Instead of Delete"; 9 | IDS_CHECK2 "Center Axis"; 10 | IDS_CHECK3 "Keep Normal Tags"; 11 | IDS_CHECK4 "Delete Original Polys"; 12 | IDS_CHECK5 "Try Re-Aligning Normals"; 13 | IDS_DIALOG_TITLE "Dialog"; 14 | IDS_DUMMY "Dummy"; 15 | IDS_GROUP_BUTTONS "Buttons"; 16 | IDS_GROUP_SETTINGS "Settings"; 17 | IDS_GROUP_WRAPPER "Wrapper"; 18 | IDS_STATIC "Group"; 19 | IDS_STATIC1 "Group"; 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generic 2 | 3 | *~ 4 | *.[oa] 5 | *.bak 6 | backup/ 7 | 8 | # Mac OS X 9 | 10 | Desktop DF 11 | Desktop DB 12 | .Spotlight-V100 13 | .DS_Store 14 | .Trashes 15 | .com.apple.timemachine.supported 16 | .fseventsd 17 | .syncinfo 18 | .TemporaryItems 19 | ._.* 20 | 21 | # Windows 22 | 23 | Thumbs.db 24 | desktop.ini 25 | 26 | # TextMate 27 | 28 | *.tm_build_errors 29 | *.tmproj 30 | 31 | # Xcode 3 32 | 33 | build/ 34 | *~.nib 35 | *.pbxuser 36 | *.perspectivev3 37 | *.mode1 38 | *.mode1v3 39 | *.mode2v3 40 | 41 | # Xcode 4 42 | 43 | xcuserdata/ 44 | *.xcworkspace 45 | 46 | # Python 47 | 48 | *.pyc 49 | __pycache__/ 50 | 51 | # Sphinx 52 | 53 | _build/ 54 | 55 | # GitHub 56 | 57 | gh-pages/ 58 | 59 | # Eclipse / Aptana 60 | 61 | *.project 62 | .settings/ 63 | doc/ 64 | 65 | # CodePro 66 | 67 | coverage.txt 68 | coverage/ 69 | 70 | # Project Specific 71 | 72 | scripts/ 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /source/py4dlib/examples/Regex Renamer 1.1/res/strings_us/dialogs/IDD_DIALOG_SETTINGS.str: -------------------------------------------------------------------------------- 1 | // C4D-DialogResource 2 | 3 | DIALOGSTRINGS IDD_DIALOG_SETTINGS 4 | { 5 | IDS_BUTTON_CANCEL "Cancel"; 6 | IDS_BUTTON_DOIT "Replace"; 7 | IDS_CHECK_DOTALL "Dot all"; 8 | IDS_CHECK_IGNORECASE "Ignore case"; 9 | IDS_CHECK_MULTILINE "Multiline"; 10 | IDS_CHECK_SELECTIONONLY "Selected objects only"; 11 | IDS_CHECK_VERBOSE "Verbose"; 12 | IDS_DIALOG_TITLE "Regex Renamer"; 13 | IDS_DUMMY "Dummy"; 14 | IDS_GROUP_BUTTONS "Buttons"; 15 | IDS_GROUP_ENTRYUI "Entry UI"; 16 | IDS_GROUP_SETTINGS_REGEX "Regex Settings"; 17 | IDS_GROUP_SETTINGS_TARGET "Target Settings"; 18 | IDS_GROUP_WRAPPER "Wrapper"; 19 | IDS_STATIC_REPLACE "Replace with:"; 20 | IDS_STATIC_SEARCH "Search for:"; 21 | } 22 | -------------------------------------------------------------------------------- /source/py4dlib/examples/Extract Polys 1.0/res/c4d_symbols.h: -------------------------------------------------------------------------------- 1 | //*********************************************************************\ 2 | // File name : c4d_symbols.h 3 | // Description : symbol definition file for the plugin 4 | // Created at : 5 | // Created by : Resource editor 6 | // Modified by : 7 | //*********************************************************************/ 8 | // WARNING : Only edit this file, if you exactly know what you are doing. 9 | // WARNING : The comments are important, too. 10 | 11 | enum 12 | { 13 | _FIRST_ELEMENT_ = 10000, 14 | // Dialog definitions of IDD_DIALOG_SETTINGS start here 15 | IDD_DIALOG_SETTINGS, 16 | IDC_BUTTON_CANCEL, 17 | IDC_BUTTON_DOIT, 18 | IDC_GROUP_WRAPPER, 19 | IDC_GROUP_SETTINGS, 20 | IDC_GROUP_BUTTONS, 21 | IDC_DUMMY, 22 | IDC_CHK_OPTIMIZE, 23 | IDC_CHK_CENTER, 24 | IDC_CHK_KEEPNORMALTAG, 25 | IDC_CHK_HIDE, 26 | IDC_CHK_DELETEORIG, 27 | IDC_CHK_RENORMALIZE, 28 | // Dialog definitions of IDD_DIALOG_SETTINGS end here 29 | 30 | 31 | // End of symbol definition 32 | _DUMMY_ELEMENT_ 33 | }; 34 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. py4dlib documentation master file, created by 2 | sphinx-quickstart on Tue Jul 30 19:53:32 2013. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to py4dlib's documentation! 7 | =================================== 8 | 9 | **py4dlib** is a collection of Python modules to ease development 10 | of scripts and plugins for MAXON's `CINEMA 4D `_ 3D application. 11 | 12 | It's goal is to provide essentials and convenience functions 13 | left out by CINEMA 4D's current Python SDK so that repetition and 14 | code duplication can be avoided. 15 | 16 | It includes routines for working with c4d objects, polygonal modelling, 17 | some utilities and tools for writing plugins and math routines needed for 3D work. 18 | 19 | 20 | Contents: 21 | 22 | .. toctree:: 23 | 24 | installation 25 | usage 26 | contributions 27 | acknowledgements 28 | license 29 | api 30 | examples 31 | 32 | 33 | Indices and tables 34 | ================== 35 | 36 | * :ref:`genindex` 37 | * :ref:`modindex` 38 | * :ref:`search` 39 | 40 | 41 | -------------------------------------------------------------------------------- /source/py4dlib/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # __init__.py 4 | # py4dlib 5 | # 6 | # Created by André Berg on 2012-09-26. 7 | # Copyright 2012 Berg Media. All rights reserved. 8 | # 9 | # andre.bergmedia@googlemail.com 10 | # 11 | # pylint: disable-msg=F0401 12 | 13 | __version__ = (0, 6, 5) 14 | __author__ = u'André Berg' 15 | __email__ = 'andre.bergmedia@googlemail.com' 16 | __url__ = 'http://github.com/andreberg/py4dlib' 17 | __license__ = 'Apache License, Version 2.0' 18 | __title__ = "py4dlib: re-usable components for CINEMA 4D's Python implementation" 19 | 20 | 21 | # Licensed under the Apache License, Version 2.0 (the "License"); 22 | # you may not use this file except in compliance with the License. 23 | # You may obtain a copy of the License at 24 | # 25 | # http://www.apache.org/licenses/LICENSE-2.0 26 | # 27 | # Unless required by applicable law or agreed to in writing, software 28 | # distributed under the License is distributed on an "AS IS" BASIS, 29 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 30 | # See the License for the specific language governing permissions and 31 | # limitations under the License. 32 | -------------------------------------------------------------------------------- /source/py4dlib/examples/Regex Renamer 1.1/res/c4d_symbols.h: -------------------------------------------------------------------------------- 1 | //*********************************************************************\ 2 | // File name : c4d_symbols.h 3 | // Description : symbol definition file for the plugin 4 | // Created at : 5 | // Created by : Resource editor 6 | // Modified by : 7 | //*********************************************************************/ 8 | // WARNING : Only edit this file, if you exactly know what you are doing. 9 | // WARNING : The comments are important, too. 10 | 11 | enum 12 | { 13 | _FIRST_ELEMENT_ = 10000, 14 | // Dialog definitions of IDD_DIALOG_SETTINGS start here 15 | IDD_DIALOG_SETTINGS, 16 | IDC_GROUP_WRAPPER, 17 | IDC_GROUP_SETTINGS, 18 | IDC_STATIC_SEARCH, 19 | IDC_EDIT_SEARCH, 20 | IDC_STATIC_REPLACE, 21 | IDC_EDIT_REPLACE, 22 | IDC_CHECK_IGNORECASE, 23 | IDC_CHECK_MULTILINE, 24 | IDC_CHECK_DOTALL, 25 | IDC_CHECK_VERBOSE, 26 | IDC_GROUP_SETTINGS2, 27 | IDC_CHECK_SELECTIONONLY, 28 | IDC_GROUP_BUTTONS, 29 | IDC_BUTTON_CANCEL, 30 | IDC_BUTTON_DOIT, 31 | IDC_DUMMY, 32 | // Dialog definitions of IDD_DIALOG_SETTINGS end here 33 | 34 | 35 | // End of symbol definition 36 | _DUMMY_ELEMENT_ 37 | }; 38 | -------------------------------------------------------------------------------- /source/py4dlib/examples/Plane Projector 1.2/res/strings_us/dialogs/IDD_DIALOG_SETTINGS.str: -------------------------------------------------------------------------------- 1 | // C4D-DialogResource 2 | 3 | DIALOGSTRINGS IDD_DIALOG_SETTINGS 4 | { 5 | IDS_BUTTON_CANCEL "Cancel"; 6 | IDS_BUTTON_DOIT "Project"; 7 | IDS_BUTTON_GETOBJ "<"; 8 | IDS_BUTTON_PICK "Pick"; 9 | IDS_BUTTON_PLANE "Plane"; 10 | IDS_BUTTON_WPLANE "Workplane"; 11 | IDS_DIALOG_TITLE "Plane Projector"; 12 | IDS_DUMMY "Dummy"; 13 | IDS_GROUP_BUTTONS "Buttons"; 14 | IDS_GROUP_MANUAL "Manual Edit Group"; 15 | IDS_GROUP_SETTINGS "Settings"; 16 | IDS_GROUP_SETTINGS2 "Settings3"; 17 | IDS_GROUP_WRAPPER "Wrapper"; 18 | IDS_MANUAL "Manual"; 19 | IDS_NORMAL "Vertex Normal"; 20 | IDS_STATIC1 "Create ..............."; 21 | IDS_STATIC2 "Create Buttons"; 22 | IDS_STATIC_MANUAL "Manual Dir .."; 23 | IDS_STATIC_MODE "Direction ....."; 24 | IDS_STATIC_SETTINGS4 "Settings4"; 25 | IDS_STATIC_TARGET "Target ..............."; 26 | IDS_STATIC_UPAXIS "Target Up Axis .."; 27 | IDS_X "X"; 28 | IDS_X_LOCAL "Local X"; 29 | IDS_Y "Y"; 30 | IDS_Y_LOCAL "Local Y"; 31 | IDS_Z "Z"; 32 | IDS_Z_LOCAL "Local Z"; 33 | } 34 | -------------------------------------------------------------------------------- /source/py4dlib/examples/Plane Projector 1.2/res/c4d_symbols.h: -------------------------------------------------------------------------------- 1 | //*********************************************************************\ 2 | // File name : c4d_symbols.h 3 | // Description : symbol definition file for the plugin 4 | // Created at : 5 | // Created by : Resource editor 6 | // Modified by : 7 | //*********************************************************************/ 8 | // WARNING : Only edit this file, if you exactly know what you are doing. 9 | // WARNING : The comments are important, too. 10 | 11 | enum 12 | { 13 | _FIRST_ELEMENT_ = 10000, 14 | // Dialog definitions of IDD_DIALOG_SETTINGS start here 15 | IDD_DIALOG_SETTINGS, 16 | IDC_GROUP_WRAPPER, 17 | IDC_GROUP_SETTINGS, 18 | IDC_STATIC_TARGET, 19 | IDC_EDIT_TARGET, 20 | IDC_BUTTON_GETOBJ, 21 | IDC_STATIC_UPAXIS, 22 | IDC_COMBO_UPAXIS, 23 | IDC_BUTTON_PLANE, 24 | IDC_BUTTON_WPLANE, 25 | IDC_GROUP_SETTINGS2, 26 | IDC_STATIC_MODE, 27 | IDC_COMBO_MODE, 28 | IDC_GROUP_SETTINGS3, 29 | IDC_STATIC_MANUAL, 30 | IDC_GROUP_MANUAL, 31 | IDC_EDIT_X, 32 | IDC_EDIT_Y, 33 | IDC_EDIT_Z, 34 | IDC_GROUP_BUTTONS, 35 | IDC_BUTTON_CANCEL, 36 | IDC_BUTTON_DOIT, 37 | IDC_DUMMY, 38 | // Dialog definitions of IDD_DIALOG_SETTINGS end here 39 | 40 | 41 | // End of symbol definition 42 | _DUMMY_ELEMENT_ 43 | }; 44 | -------------------------------------------------------------------------------- /source/py4dlib/examples/Extract Polys 1.0/res/dialogs/IDD_DIALOG_SETTINGS.res: -------------------------------------------------------------------------------- 1 | // C4D-DialogResource 2 | DIALOG IDD_DIALOG_SETTINGS 3 | { 4 | NAME IDS_DIALOG_TITLE; CENTER_V; ALIGN_LEFT; 5 | 6 | GROUP IDC_GROUP_WRAPPER 7 | { 8 | NAME IDS_GROUP_WRAPPER; ALIGN_TOP; SCALE_H; 9 | BORDERSIZE 4, 4, 4, 4; 10 | COLUMNS 1; 11 | SPACE 4, 4; 12 | 13 | GROUP IDC_GROUP_SETTINGS 14 | { 15 | NAME IDS_GROUP_SETTINGS; ALIGN_TOP; CENTER_H; 16 | BORDERSIZE 5, 5, 5, 5; 17 | COLUMNS 1; 18 | SPACE 4, 4; 19 | 20 | CHECKBOX IDC_CHK_OPTIMIZE { NAME IDS_CHECK; ALIGN_TOP; ALIGN_LEFT; } 21 | CHECKBOX IDC_CHK_CENTER { NAME IDS_CHECK2; ALIGN_TOP; ALIGN_LEFT; } 22 | SEPARATOR { SCALE_H; } 23 | CHECKBOX IDC_CHK_DELETEORIG { NAME IDS_CHECK4; ALIGN_TOP; ALIGN_LEFT; } 24 | GROUP IDC_STATIC 25 | { 26 | ALIGN_TOP; ALIGN_LEFT; 27 | BORDERSIZE 16, 0, 0, 0; 28 | COLUMNS 1; 29 | SPACE 4, 4; 30 | 31 | CHECKBOX IDC_CHK_HIDE { NAME IDS_CHECK1; ALIGN_TOP; ALIGN_LEFT; } 32 | } 33 | SEPARATOR { SCALE_H; } 34 | CHECKBOX IDC_CHK_KEEPNORMALTAG { NAME IDS_CHECK3; ALIGN_TOP; ALIGN_LEFT; } 35 | GROUP IDC_STATIC 36 | { 37 | NAME IDS_STATIC1; ALIGN_TOP; ALIGN_LEFT; 38 | BORDERSIZE 16, 0, 0, 0; 39 | COLUMNS 1; 40 | SPACE 4, 4; 41 | 42 | CHECKBOX IDC_CHK_RENORMALIZE { NAME IDS_CHECK5; ALIGN_TOP; ALIGN_LEFT; } 43 | } 44 | } 45 | SEPARATOR { SCALE_H; } 46 | GROUP IDC_GROUP_BUTTONS 47 | { 48 | NAME IDS_GROUP_BUTTONS; ALIGN_TOP; CENTER_H; 49 | BORDERSIZE 0, 0, 0, 0; 50 | COLUMNS 2; 51 | SPACE 4, 4; 52 | 53 | BUTTON IDC_BUTTON_CANCEL { NAME IDS_BUTTON_CANCEL; ALIGN_TOP; CENTER_H; } 54 | BUTTON IDC_BUTTON_DOIT { NAME IDS_BUTTON_DOIT; ALIGN_TOP; CENTER_H; } 55 | } 56 | GROUP IDC_DUMMY 57 | { 58 | NAME IDS_DUMMY; ALIGN_TOP; SCALE_H; 59 | BORDERSIZE 0, 0, 0, 0; 60 | COLUMNS 1; 61 | SPACE 4, 4; 62 | 63 | 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | .. _py4dlib.examples: 2 | 3 | Examples 4 | -------- 5 | 6 | ShowPolygonNumber 7 | ~~~~~~~~~~~~~~~~~ 8 | 9 | Example script that shows how to get point and polygon selections 10 | and how to create objects and attach them to polygons in local and 11 | global coordinate systems. 12 | 13 | 14 | PrintObjectHierarchy 15 | ~~~~~~~~~~~~~~~~~~~~ 16 | 17 | Example script that shows how to use the :py:class:`ObjectHierarchy` class. 18 | 19 | .. _Extract: 20 | 21 | Extract 22 | ~~~~~~~ 23 | 24 | Example plugin that extracts selected polygons across multiple objects 25 | into new polygon objects. 26 | 27 | Basically, the same as selecting polygons on some objects, 28 | running Split, then deleting the inverse selection to keep 29 | only the selected polys. Plus a few extras. 30 | 31 | PlaneProjector 32 | ~~~~~~~~~~~~~~ 33 | 34 | Example plugin that shows how to project selected points along a direction vector 35 | onto a planar target. 36 | 37 | Can snap any number of points onto planar targets along local or global X, Y, Z, a manual 38 | direction or the averaged vertex normals (basically the direction of the modelling tool's 39 | Normal setting for a group of selected points). 40 | 41 | Currently needs a working plane or a plane object as target. You can create a new working 42 | plane yourself and pick it up to be the target by pressing the pick target button (<) or 43 | you can select any number of polygons on any number of polygon objects and hit one of the 44 | Create buttons to create a new plane or workplane aligned to the polygons. 45 | 46 | Note: in R14 the workplane button aligns the workplane mode to the selection instead of 47 | creating a workplane object. 48 | 49 | As a future improvement it shouldn't be hard at all to use polygons of other objects as 50 | planar targets. 51 | 52 | RegexRenamer 53 | ~~~~~~~~~~~~ 54 | 55 | Regex Renamer is a command plugin, that shows how to use the :py:class:`UserDefaults` class 56 | to provide UI state persistence between times the plugin is run. 57 | 58 | The plugin's function is to utilize Python's powerful "re" module to perform regular expression 59 | based searching and replacing within object names. 60 | 61 | 62 | Check out `http://regular-expression.info `_ to learn more 63 | about regular expressions. 64 | 65 | -------------------------------------------------------------------------------- /source/test/objects/object_hierarchy_tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # object_hierarchy_tests.py 4 | # py4dlib 5 | # 6 | # Created by André Berg on 2012-09-28. 7 | # Copyright 2013 Berg Media. All rights reserved. 8 | # 9 | # This file is a copy of the Python tag script 10 | # found in the C4D file of the same name. 11 | # It is provided for reference in case CINEMA 4D 12 | # is not available or the .c4d file can't be opened. 13 | # 14 | # pylint: disable-msg=F0401,W0611 15 | 16 | import os 17 | import pprint 18 | 19 | pp = pprint.PrettyPrinter(width=92) 20 | PP = pp.pprint 21 | PF = pp.pformat 22 | 23 | DEBUG = 1 or ('DebugLevel' in os.environ and os.environ['DebugLevel'] > 0) 24 | TESTRUN = 0 or ('TestRunLevel' in os.environ and os.environ['TestRunLevel'] > 0) 25 | 26 | try: 27 | import c4d #@UnresolvedImport 28 | from c4d import documents #@UnresolvedImport 29 | except ImportError: 30 | if TESTRUN == 1: 31 | pass 32 | 33 | from py4dlib import utils, objects #@UnusedImport 34 | from py4dlib.objects import ObjectHierarchy 35 | 36 | 37 | def main(): 38 | utils.ClearConsole() 39 | doc = documents.GetActiveDocument() 40 | if not doc: 41 | return None 42 | targetobj = doc.SearchObject('Target') 43 | sourceobj = doc.SearchObject('Source') 44 | if not targetobj or not sourceobj: 45 | return None 46 | oh = ObjectHierarchy(targetobj) 47 | oh.PPrint() 48 | oh.PPrint(filter_type=c4d.Onull) 49 | scene = ObjectHierarchy() 50 | PP(scene) 51 | print(scene) 52 | filtered_objs = oh.Get('Target/Source/../*') 53 | for obj in filtered_objs: 54 | print(obj.GetName()) 55 | 56 | 57 | if __name__=='__main__': 58 | main() 59 | 60 | 61 | # Licensed under the Apache License, Version 2.0 (the "License"); 62 | # you may not use this file except in compliance with the License. 63 | # You may obtain a copy of the License at 64 | # 65 | # http://www.apache.org/licenses/LICENSE-2.0 66 | # 67 | # Unless required by applicable law or agreed to in writing, software 68 | # distributed under the License is distributed on an "AS IS" BASIS, 69 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 70 | # See the License for the specific language governing permissions and 71 | # limitations under the License. 72 | -------------------------------------------------------------------------------- /source/test/utils_tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # test.utils_tests 4 | # project 5 | # 6 | # Created by André Berg on 2013-08-12. 7 | # Copyright 2013 Berg Media. All rights reserved. 8 | # 9 | # andre.bergmedia@googlemail.com 10 | # 11 | # pylint: disable-msg=F0401 12 | 13 | import os 14 | import unittest 15 | 16 | __version__ = (0, 1) 17 | __date__ = '2013-08-12' 18 | __updated__ = '2013-08-12' 19 | 20 | 21 | DEBUG = 0 or ('DebugLevel' in os.environ and os.environ['DebugLevel'] > 0) 22 | TESTRUN = 1 or ('TestRunLevel' in os.environ and os.environ['TestRunLevel'] > 0) 23 | 24 | 25 | from py4dlib import utils 26 | from py4dlib.utils import EscapeUnicode, UnescapeUnicode 27 | 28 | 29 | class Test(unittest.TestCase): 30 | 31 | def testEscapeUnescapeUnicode(self): 32 | 33 | a1 = "W\xfcrfel" # R12 latin-1 bytes 34 | u1 = u"W\xfcrfel" # R12 latin-1 unicode 35 | a2 = r"W\u00FCrfel" # R12 bytes escaped unicode 36 | a3 = "W\xc3\xbcrfel" # R13/14 utf-8 bytes 37 | u2 = u"Würfel" # R13/14 utf-8 unicode 38 | 39 | src = [a1, u1, a2, a3, u2] 40 | 41 | dst12 = [] 42 | for s in src: 43 | dst12.append(EscapeUnicode(s)) 44 | 45 | utils.C4D_VERSION = 13000 46 | 47 | dst13 = [] 48 | for s in src: 49 | dst13.append(EscapeUnicode(s)) 50 | 51 | for d in dst12: 52 | self.assertEquals(r"W\u00FCrfel", d) 53 | 54 | dst13_expected = src 55 | 56 | for i in xrange(len(dst13)): 57 | self.assertEquals(dst13[i], dst13_expected[i]) 58 | 59 | utils.C4D_VERSION = 12000 60 | 61 | for j in dst12: 62 | res = UnescapeUnicode(j) 63 | self.assertEquals(res, u"W\xfcrfel") 64 | 65 | 66 | if __name__ == "__main__": 67 | #import sys;sys.argv = ['', 'Test.testName'] 68 | unittest.main() 69 | 70 | 71 | # Licensed under the Apache License, Version 2.0 (the "License"); 72 | # you may not use this file except in compliance with the License. 73 | # You may obtain a copy of the License at 74 | # 75 | # http://www.apache.org/licenses/LICENSE-2.0 76 | # 77 | # Unless required by applicable law or agreed to in writing, software 78 | # distributed under the License is distributed on an "AS IS" BASIS, 79 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 80 | # See the License for the specific language governing permissions and 81 | # limitations under the License. 82 | -------------------------------------------------------------------------------- /source/py4dlib/examples/Regex Renamer 1.1/res/dialogs/IDD_DIALOG_SETTINGS.res: -------------------------------------------------------------------------------- 1 | // C4D-DialogResource 2 | DIALOG IDD_DIALOG_SETTINGS 3 | { 4 | NAME IDS_DIALOG_TITLE; CENTER_V; ALIGN_LEFT; 5 | 6 | GROUP IDC_GROUP_WRAPPER 7 | { 8 | NAME IDS_GROUP_WRAPPER; SCALE_V; SCALE_H; 9 | BORDERSIZE 4, 4, 4, 4; 10 | COLUMNS 1; 11 | SPACE 4, 4; 12 | 13 | GROUP IDC_GROUP_SETTINGS 14 | { 15 | NAME IDS_GROUP_ENTRYUI; SCALE_V; SCALE_H; 16 | BORDERSIZE 2, 2, 2, 2; 17 | COLUMNS 2; 18 | SPACE 4, 4; 19 | 20 | STATICTEXT IDC_STATIC_SEARCH { NAME IDS_STATIC_SEARCH; CENTER_V; ALIGN_RIGHT; } 21 | MULTILINEEDIT IDC_EDIT_SEARCH 22 | { 23 | SCALE_V; SCALE_H; SIZE 70, 0; 24 | MONOSPACED; SYNTAXCOLOR; PYTHON; STATUSBAR; 25 | } 26 | STATICTEXT IDC_STATIC_REPLACE { NAME IDS_STATIC_REPLACE; CENTER_V; ALIGN_RIGHT; } 27 | MULTILINEEDIT IDC_EDIT_REPLACE 28 | { 29 | SCALE_V; SCALE_H; SIZE 70, 0; 30 | MONOSPACED; SYNTAXCOLOR; PYTHON; STATUSBAR; 31 | } 32 | } 33 | GROUP IDC_STATIC 34 | { 35 | NAME IDS_GROUP_SETTINGS_REGEX; ALIGN_TOP; ALIGN_LEFT; 36 | BORDERSTYLE BORDER_NONE; BORDERSIZE 77, 0, 4, 0; 37 | ROWS 1; 38 | 39 | CHECKBOX IDC_CHECK_IGNORECASE { NAME IDS_CHECK_IGNORECASE; ALIGN_TOP; ALIGN_LEFT; } 40 | CHECKBOX IDC_CHECK_MULTILINE { NAME IDS_CHECK_MULTILINE; ALIGN_TOP; ALIGN_LEFT; } 41 | CHECKBOX IDC_CHECK_DOTALL { NAME IDS_CHECK_DOTALL; ALIGN_TOP; ALIGN_LEFT; } 42 | CHECKBOX IDC_CHECK_VERBOSE { NAME IDS_CHECK_VERBOSE; ALIGN_TOP; ALIGN_LEFT; } 43 | } 44 | GROUP IDC_GROUP_SETTINGS2 45 | { 46 | NAME IDS_GROUP_SETTINGS_TARGET; ALIGN_TOP; ALIGN_LEFT; 47 | BORDERSIZE 77, 0, 0, 0; 48 | COLUMNS 1; 49 | SPACE 4, 4; 50 | 51 | CHECKBOX IDC_CHECK_SELECTIONONLY { NAME IDS_CHECK_SELECTIONONLY; ALIGN_TOP; ALIGN_LEFT; } 52 | } 53 | SEPARATOR { SCALE_H; } 54 | GROUP IDC_GROUP_BUTTONS 55 | { 56 | NAME IDS_GROUP_BUTTONS; ALIGN_TOP; CENTER_H; 57 | BORDERSIZE 0, 0, 0, 0; 58 | COLUMNS 2; 59 | SPACE 4, 4; 60 | 61 | BUTTON IDC_BUTTON_CANCEL { NAME IDS_BUTTON_CANCEL; ALIGN_TOP; CENTER_H; } 62 | BUTTON IDC_BUTTON_DOIT { NAME IDS_BUTTON_DOIT; ALIGN_TOP; CENTER_H; } 63 | } 64 | GROUP IDC_DUMMY 65 | { 66 | NAME IDS_DUMMY; ALIGN_TOP; SCALE_H; 67 | BORDERSIZE 0, 0, 0, 0; 68 | COLUMNS 1; 69 | SPACE 4, 4; 70 | 71 | 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /source/py4dlib/examples/PrintObjectHierarchy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # PrintObjectHierarchy.py 4 | # py4dlib.examples 5 | # 6 | # Created by André Berg on 2013-07-29. 7 | # Copyright 2013 Berg Media. All rights reserved. 8 | # 9 | # andre.bergmedia@googlemail.com 10 | # 11 | # PrintObjectHierarchy 12 | # 13 | # Summary: 14 | # 15 | # Shows how to use the ObjectHierarchy class. 16 | # 17 | # pylint: disable-msg=F0401 18 | 19 | """ 20 | Name-US:Print Object Hierarchy 21 | Description-US:Pretty print an indented list of the selected object hierarchy to the console. 22 | """ 23 | 24 | import os 25 | 26 | __version__ = (0, 2) 27 | __date__ = '2013-07-29' 28 | __updated__ = '2013-08-08' 29 | 30 | 31 | DEBUG = 1 or ('DebugLevel' in os.environ and os.environ['DebugLevel'] > 0) 32 | TESTRUN = 0 or ('TestRunLevel' in os.environ and os.environ['TestRunLevel'] > 0) 33 | 34 | 35 | 36 | import pprint 37 | 38 | pp = pprint.PrettyPrinter(width=92) 39 | PP = pp.pprint 40 | PF = pp.pformat 41 | 42 | try: 43 | import c4d #@UnresolvedImport 44 | from c4d import documents 45 | except ImportError: 46 | if TESTRUN == 1: 47 | pass 48 | 49 | 50 | if DEBUG: 51 | import py4dlib 52 | reload(py4dlib) 53 | reload(py4dlib.objects) 54 | 55 | from py4dlib.objects import ObjectHierarchy 56 | from py4dlib.utils import ClearConsole, UnescapeUnicode, EscapeUnicode 57 | 58 | 59 | def main(doc): # IGNORE:W0621 60 | if not doc: return None 61 | doc.StartUndo() 62 | 63 | sel = doc.GetSelection() 64 | if sel is None: return False 65 | 66 | c4d.StopAllThreads() 67 | 68 | root = doc.GetFirstObject() 69 | if not root: return False 70 | 71 | print(u"printing hierarchy starting from %s" % root.GetName()) 72 | 73 | oh = ObjectHierarchy() 74 | oh.PPrint() 75 | 76 | print(u"printing Null objects starting from %s" % root.GetName()) 77 | 78 | oh.PPrint(filter_type=c4d.Onull) 79 | 80 | PP(oh) 81 | print(oh) 82 | 83 | filteredObjs = oh.Get('*/*') 84 | for obj in filteredObjs: 85 | print obj.GetName() 86 | 87 | # tell C4D to update internal state 88 | c4d.EventAdd() 89 | doc.EndUndo() 90 | 91 | 92 | if __name__ == '__main__': 93 | ClearConsole() 94 | doc = documents.GetActiveDocument() 95 | main(doc) 96 | 97 | 98 | 99 | # Licensed under the Apache License, Version 2.0 (the "License"); 100 | # you may not use this file except in compliance with the License. 101 | # You may obtain a copy of the License at 102 | # 103 | # http://www.apache.org/licenses/LICENSE-2.0 104 | # 105 | # Unless required by applicable law or agreed to in writing, software 106 | # distributed under the License is distributed on an "AS IS" BASIS, 107 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 108 | # See the License for the specific language governing permissions and 109 | # limitations under the License. 110 | -------------------------------------------------------------------------------- /source/test/maths/matrix_to_listlist_tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # matrix_to_listlist_tests.py 4 | # py4dlib 5 | # 6 | # Created by André Berg on 2013-08-02. 7 | # Copyright 2013 Berg Media. All rights reserved. 8 | # 9 | # This file is a copy of a Python script 10 | # to be run on the C4D file of the same name. 11 | # 12 | # pylint: disable-msg=F0401,W0611 13 | 14 | import os 15 | 16 | DEBUG = 1 or ('DebugLevel' in os.environ and os.environ['DebugLevel'] > 0) 17 | TESTRUN = 0 or ('TestRunLevel' in os.environ and os.environ['TestRunLevel'] > 0) 18 | 19 | try: 20 | import c4d #@UnresolvedImport 21 | from c4d import documents #@UnresolvedImport 22 | except ImportError: 23 | if TESTRUN == 1: 24 | pass 25 | 26 | import py4dlib 27 | from py4dlib.utils import PPLLString, ClearConsole 28 | from py4dlib.maths import MatrixToListList, ListListToMatrix 29 | from py4dlib.objects import CreateObject, Select 30 | 31 | if DEBUG: 32 | reload(py4dlib) 33 | reload(py4dlib.maths) 34 | reload(py4dlib.utils) 35 | 36 | 37 | def main(): 38 | ClearConsole() 39 | doc = documents.GetActiveDocument() 40 | op = doc.SearchObject("Cube") 41 | if op is not None: 42 | print("Removing old Cube...") 43 | op.Remove() 44 | print("Creating new Cube...") 45 | op = CreateObject(c4d.Ocube, "Cube") 46 | Select(op) 47 | 48 | print("") 49 | 50 | mg = op.GetMg() 51 | n_off = c4d.Vector(100, 0, 0) 52 | n_mg = c4d.Matrix(n_off, mg.v1, mg.v2, mg.v3) 53 | 54 | print("Setting new mg from manually constructed Matrix:") 55 | print(" %r" % n_mg) 56 | op.SetMg(n_mg) 57 | 58 | print("") 59 | 60 | print("Converting matrix n_mg to list:") 61 | n_mgll = MatrixToListList(n_mg, incl_off=True) 62 | print("") 63 | 64 | print("Displaying new mg as list:") 65 | print(PPLLString(n_mgll)) 66 | print("") 67 | 68 | print("Setting new global matrix from list:") 69 | n_ll = [[200.0, 10.0, 0.0], 70 | [ 2.0, 0.0, 0.0], 71 | [ 0.0, 2.0, 0.0], 72 | [ 0.0, 0.0, 2.0]] 73 | print(PPLLString(n_ll)) 74 | print("") 75 | 76 | print("Converting list structure to Matrix n_llmg") 77 | n_llmg = ListListToMatrix(n_ll) 78 | print(" %r" % n_llmg) 79 | print("") 80 | 81 | print("Setting n_llmg") 82 | print("") 83 | op.SetMg(n_llmg) 84 | 85 | print("assert op.GetMg().off.x == 200") 86 | 87 | assert(op.GetMg().off.x == 200) 88 | print("True") 89 | 90 | c4d.EventAdd() 91 | 92 | if __name__=='__main__': 93 | main() 94 | 95 | 96 | # Licensed under the Apache License, Version 2.0 (the "License"); 97 | # you may not use this file except in compliance with the License. 98 | # You may obtain a copy of the License at 99 | # 100 | # http://www.apache.org/licenses/LICENSE-2.0 101 | # 102 | # Unless required by applicable law or agreed to in writing, software 103 | # distributed under the License is distributed on an "AS IS" BASIS, 104 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 105 | # See the License for the specific language governing permissions and 106 | # limitations under the License. 107 | -------------------------------------------------------------------------------- /docs/pydocs3theme/static/pydoctheme.css_t: -------------------------------------------------------------------------------- 1 | @import url("default.css"); 2 | 3 | body { 4 | background-color: white; 5 | margin-left: 1em; 6 | margin-right: 1em; 7 | } 8 | 9 | div.related { 10 | margin-bottom: 1.2em; 11 | padding: 0.5em 0; 12 | border-top: 1px solid #ccc; 13 | margin-top: 0.5em; 14 | } 15 | 16 | div.related a:hover { 17 | color: #0095C4; 18 | } 19 | 20 | div.related:first-child { 21 | border-top: 0; 22 | border-bottom: 1px solid #ccc; 23 | } 24 | 25 | div.sphinxsidebar { 26 | background-color: #eeeeee; 27 | border-radius: 5px; 28 | line-height: 130%; 29 | font-size: smaller; 30 | } 31 | 32 | div.sphinxsidebar h3, div.sphinxsidebar h4 { 33 | margin-top: 1.5em; 34 | } 35 | 36 | div.sphinxsidebarwrapper > h3:first-child { 37 | margin-top: 0.2em; 38 | } 39 | 40 | div.sphinxsidebarwrapper > ul > li > ul > li { 41 | margin-bottom: 0.4em; 42 | } 43 | 44 | div.sphinxsidebar a:hover { 45 | color: #0095C4; 46 | } 47 | 48 | div.sphinxsidebar input { 49 | font-family: 'Lucida Grande',Arial,sans-serif; 50 | border: 1px solid #999999; 51 | font-size: smaller; 52 | border-radius: 3px; 53 | } 54 | 55 | div.sphinxsidebar input[type=text] { 56 | max-width: 150px; 57 | } 58 | 59 | div.body { 60 | padding: 0 0 0 1.2em; 61 | } 62 | 63 | div.body p { 64 | line-height: 140%; 65 | } 66 | 67 | div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 { 68 | margin: 0; 69 | border: 0; 70 | padding: 0.3em 0; 71 | } 72 | 73 | div.body hr { 74 | border: 0; 75 | background-color: #ccc; 76 | height: 1px; 77 | } 78 | 79 | div.body pre { 80 | border-radius: 3px; 81 | border: 1px solid #ac9; 82 | } 83 | 84 | div.body div.admonition, div.body div.impl-detail { 85 | border-radius: 3px; 86 | } 87 | 88 | div.body div.impl-detail > p { 89 | margin: 0; 90 | } 91 | 92 | div.body div.seealso { 93 | border: 1px solid #dddd66; 94 | } 95 | 96 | div.body a { 97 | color: #00608f; 98 | } 99 | 100 | div.body a:visited { 101 | color: #30306f; 102 | } 103 | 104 | div.body a:hover { 105 | color: #00B0E4; 106 | } 107 | 108 | tt, pre { 109 | font-family: monospace, sans-serif; 110 | font-size: 96.5%; 111 | } 112 | 113 | div.body tt { 114 | border-radius: 3px; 115 | } 116 | 117 | div.body tt.descname { 118 | font-size: 120%; 119 | } 120 | 121 | div.body tt.xref, div.body a tt { 122 | font-weight: normal; 123 | } 124 | 125 | p.deprecated { 126 | border-radius: 3px; 127 | } 128 | 129 | table.docutils { 130 | border: 1px solid #ddd; 131 | min-width: 20%; 132 | border-radius: 3px; 133 | margin-top: 10px; 134 | margin-bottom: 10px; 135 | } 136 | 137 | table.docutils td, table.docutils th { 138 | border: 1px solid #ddd !important; 139 | border-radius: 3px; 140 | } 141 | 142 | table p, table li { 143 | text-align: left !important; 144 | } 145 | 146 | table.docutils th { 147 | background-color: #eee; 148 | padding: 0.3em 0.5em; 149 | } 150 | 151 | table.docutils td { 152 | background-color: white; 153 | padding: 0.3em 0.5em; 154 | } 155 | 156 | table.footnote, table.footnote td { 157 | border: 0 !important; 158 | } 159 | 160 | div.footer { 161 | line-height: 150%; 162 | margin-top: -2em; 163 | text-align: right; 164 | width: auto; 165 | margin-right: 10px; 166 | } 167 | 168 | div.footer a:hover { 169 | color: #0095C4; 170 | } 171 | -------------------------------------------------------------------------------- /source/test/objects/center_object_axis_tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # center_object_axis_tests.py 4 | # py4dlib 5 | # 6 | # Created by André Berg on 2013-08-02. 7 | # Copyright 2013 Berg Media. All rights reserved. 8 | # 9 | # This file is a copy of a Python script 10 | # to be run on the C4D file of the same name. 11 | # 12 | # pylint: disable-msg=F0401,W0611,E1103 13 | 14 | import os 15 | 16 | DEBUG = 1 or ('DebugLevel' in os.environ and os.environ['DebugLevel'] > 0) 17 | TESTRUN = 0 or ('TestRunLevel' in os.environ and os.environ['TestRunLevel'] > 0) 18 | 19 | try: 20 | import c4d #@UnresolvedImport 21 | from c4d import documents #@UnresolvedImport 22 | except ImportError: 23 | if TESTRUN == 1: 24 | pass 25 | 26 | 27 | import py4dlib 28 | from py4dlib.utils import ClearConsole 29 | from py4dlib.maths import BBox 30 | from py4dlib.objects import CreateReplaceObject, CenterObjectAxis, MakeEditable 31 | from py4dlib.mesh import CalcGravityCenter 32 | 33 | 34 | if DEBUG: 35 | reload(py4dlib) 36 | reload(py4dlib.maths) 37 | reload(py4dlib.mesh) 38 | reload(py4dlib.utils) 39 | reload(py4dlib.objects) 40 | 41 | 42 | def main(): 43 | doc = documents.GetActiveDocument() 44 | if doc is None: 45 | return False 46 | 47 | doc.StartUndo() 48 | 49 | op = CreateReplaceObject(c4d.Osphere, "TestSphere") 50 | if op is None: 51 | return False 52 | 53 | op = MakeEditable(op) 54 | 55 | which_p = 1 56 | trans = c4d.Vector(0, 100, 0) 57 | 58 | doc.AddUndo(c4d.UNDOTYPE_CHANGE, op) 59 | op.SetPoint(which_p, op.GetPoint(which_p) + trans) 60 | 61 | CenterObjectAxis(op, center="midpoint") 62 | 63 | op_mg = op.GetMg() 64 | op_name = op.GetName() 65 | 66 | bb = BBox.FromObject(op) 67 | mp = bb.midpoint 68 | cg = CalcGravityCenter(op) 69 | 70 | # no need to multiply with op_mg since we'll insert under TestSphere later 71 | mp_off = mp # * op_mg 72 | cg_off = cg # * op_mg 73 | 74 | mp_mg = c4d.Matrix(mp_off, op_mg.v1, op_mg.v2, op_mg.v3) 75 | cg_mg = c4d.Matrix(cg_off, op_mg.v1, op_mg.v2, op_mg.v3) 76 | 77 | print("midpoint in local coords = %r" % mp_off) 78 | print("midpoint in global coords = %r" % (mp_off * op_mg)) 79 | 80 | print("grav center in local coords = %r" % cg_off) 81 | print("grav center in global coords = %r" % (cg_off * op_mg)) 82 | 83 | mp_null = CreateReplaceObject(c4d.Onull, "%s Midpoint" % op_name) 84 | cg_null = CreateReplaceObject(c4d.Onull, "%s Gravity Center" % op_name) 85 | 86 | cg_null[c4d.NULLOBJECT_DISPLAY] = 7 # Hexagon 87 | cg_null[c4d.NULLOBJECT_RADIUS] = 125 88 | 89 | mp_null[c4d.NULLOBJECT_DISPLAY] = 2 # Circle 90 | mp_null[c4d.NULLOBJECT_RADIUS] = 100 91 | 92 | mp_null.SetMg(mp_mg) 93 | cg_null.SetMg(cg_mg) 94 | 95 | mp_null.InsertUnder(op) 96 | cg_null.InsertUnder(op) 97 | 98 | print("assert(mp_null.GetMg().off == c4d.Vector(0, 50, 0))") 99 | assert(mp_null.GetMg().off == c4d.Vector(0, 50, 0)) 100 | 101 | print("True") 102 | 103 | doc.EndUndo() 104 | doc.Message(c4d.MSG_UPDATE) 105 | 106 | if __name__ == '__main__': 107 | ClearConsole() 108 | main() 109 | 110 | 111 | 112 | # Licensed under the Apache License, Version 2.0 (the "License"); 113 | # you may not use this file except in compliance with the License. 114 | # You may obtain a copy of the License at 115 | # 116 | # http://www.apache.org/licenses/LICENSE-2.0 117 | # 118 | # Unless required by applicable law or agreed to in writing, software 119 | # distributed under the License is distributed on an "AS IS" BASIS, 120 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 121 | # See the License for the specific language governing permissions and 122 | # limitations under the License. 123 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API docs 2 | -------- 3 | 4 | A word about the style of the API. I tried to mirror the format and style 5 | used by the original Py4D SDK so using methods from **py4dlib** should 6 | hopefully feel familiar. 7 | 8 | Concerning function/method paramaters, I also try to avoid ambiguity by using 9 | the same argument names for the same type of data. 10 | 11 | This is especially apparent for functions that expect points or polygons. 12 | I usually try set the functions up in a general way, so that you don't have to 13 | remember that you have to call the version of the function that expects a list 14 | of point indices instead of a list of points (``c4d.Vector``, to be exact). 15 | 16 | A polymorphic nature like that is usually indicated by using the parameter name ``e`` 17 | (for *element* or *entity*) whenever you can pass either. 18 | 19 | In all other cases it should be apparent if you need to pass a point, a list of 20 | points, a list of point indices, an object, a matrix, vector or whatever else have you. 21 | 22 | Here's an overview of some of the default parameter names and their meaning: 23 | 24 | +---------+---------------------------------------------------------------------------+ 25 | | e | the ``any`` value. stands for *element*. Refer to the function docs what | 26 | | | any means in the function's context. | 27 | +---------+---------------------------------------------------------------------------+ 28 | | obj | any descendant of ``c4d.BaseObject`` incl. ``c4d.PointObject`` + children | 29 | +---------+---------------------------------------------------------------------------+ 30 | | m | ``c4d.Matrix`` | 31 | +---------+---------------------------------------------------------------------------+ 32 | | v | ``c4d.Vector`` | 33 | +---------+---------------------------------------------------------------------------+ 34 | | p | ``c4d.Vector`` representing a point, or a polygon as ``c4d.CPolygon`` | 35 | +---------+---------------------------------------------------------------------------+ 36 | | l | ``list`` | 37 | +---------+---------------------------------------------------------------------------+ 38 | | li | ``list`` of ``int``, which usually represents indices into an object's | 39 | | | point list | 40 | +---------+---------------------------------------------------------------------------+ 41 | | lv | ``list`` of vectors | 42 | +---------+---------------------------------------------------------------------------+ 43 | | lp | ``list`` of points | 44 | +---------+---------------------------------------------------------------------------+ 45 | | lli | ``list`` of ``list`` of ``int`` | 46 | +---------+---------------------------------------------------------------------------+ 47 | | llf | ``list`` of ``list`` of ``float`` | 48 | +---------+---------------------------------------------------------------------------+ 49 | | x, n, w | ``float`` sometimes can be called other traditional names, like *theta* | 50 | | ... | if for example an angle is represented (w for greek omega, n for number) | 51 | +---------+---------------------------------------------------------------------------+ 52 | | a, b, c | a name series usually represents a series of vectors. | 53 | | ... | | 54 | +---------+---------------------------------------------------------------------------+ 55 | 56 | Contents: 57 | 58 | .. toctree:: 59 | 60 | api/maths 61 | api/mesh 62 | api/objects 63 | api/plugins 64 | api/utils 65 | -------------------------------------------------------------------------------- /source/py4dlib/examples/Plane Projector 1.2/res/dialogs/IDD_DIALOG_SETTINGS.res: -------------------------------------------------------------------------------- 1 | // C4D-DialogResource 2 | DIALOG IDD_DIALOG_SETTINGS 3 | { 4 | NAME IDS_DIALOG_TITLE; CENTER_V; ALIGN_LEFT; 5 | 6 | GROUP IDC_GROUP_WRAPPER 7 | { 8 | NAME IDS_GROUP_WRAPPER; ALIGN_TOP; SCALE_H; 9 | BORDERSIZE 4, 4, 4, 4; 10 | COLUMNS 1; 11 | SPACE 4, 4; 12 | 13 | GROUP IDC_GROUP_SETTINGS 14 | { 15 | NAME IDS_GROUP_SETTINGS; ALIGN_TOP; SCALE_H; 16 | BORDERSIZE 5, 5, 5, 0; 17 | COLUMNS 3; 18 | SPACE 4, 4; 19 | 20 | STATICTEXT IDC_STATIC_TARGET { NAME IDS_STATIC_TARGET; CENTER_V; ALIGN_LEFT; } 21 | EDITTEXT IDC_EDIT_TARGET 22 | { CENTER_V; SCALE_H; SIZE 70, 0; } 23 | BUTTON IDC_BUTTON_GETOBJ { NAME IDS_BUTTON_GETOBJ; ALIGN_TOP; ALIGN_LEFT; } 24 | } 25 | GROUP IDC_STATIC 26 | { 27 | NAME IDS_STATIC_SETTINGS4; ALIGN_TOP; SCALE_H; 28 | BORDERSIZE 5, 0, 5, 0; 29 | COLUMNS 2; 30 | SPACE 4, 4; 31 | 32 | STATICTEXT IDC_STATIC_UPAXIS { NAME IDS_STATIC_UPAXIS; CENTER_V; ALIGN_LEFT; } 33 | COMBOBOX IDC_COMBO_UPAXIS 34 | { 35 | ALIGN_TOP; SCALE_H; SIZE 150, 0; 36 | CHILDS 37 | { 38 | 1, IDS_X; 39 | 2, IDS_Y; 40 | 3, IDS_Z; 41 | } 42 | } 43 | STATICTEXT IDC_STATIC { NAME IDS_STATIC1; CENTER_V; ALIGN_LEFT; } 44 | GROUP IDC_STATIC 45 | { 46 | NAME IDS_STATIC2; ALIGN_TOP; SCALE_H; 47 | BORDERSTYLE BORDER_NONE; BORDERSIZE 0, 0, 0, 0; 48 | COLUMNS 3; 49 | 50 | BUTTON IDC_BUTTON_PLANE { NAME IDS_BUTTON_PLANE; ALIGN_TOP; SCALE_H; } 51 | BUTTON IDC_BUTTON_WPLANE { NAME IDS_BUTTON_WPLANE; ALIGN_TOP; SCALE_H; } 52 | } 53 | } 54 | SEPARATOR { SCALE_H; } 55 | GROUP IDC_GROUP_SETTINGS2 56 | { 57 | NAME IDS_GROUP_SETTINGS2; ALIGN_TOP; SCALE_H; 58 | BORDERSIZE 5, 0, 5, 0; 59 | COLUMNS 2; 60 | SPACE 4, 4; 61 | 62 | STATICTEXT IDC_STATIC_MODE { NAME IDS_STATIC_MODE; CENTER_V; ALIGN_LEFT; } 63 | COMBOBOX IDC_COMBO_MODE 64 | { 65 | ALIGN_TOP; SCALE_H; SIZE 150, 0; 66 | CHILDS 67 | { 68 | 1, IDS_NORMAL; 69 | 2, IDS_MANUAL; 70 | 3, IDS_X; 71 | 4, IDS_Y; 72 | 5, IDS_Z; 73 | 6, IDS_X_LOCAL; 74 | 7, IDS_Y_LOCAL; 75 | 8, IDS_Z_LOCAL; 76 | } 77 | } 78 | } 79 | GROUP IDC_GROUP_SETTINGS3 80 | { 81 | NAME IDS_GROUP_SETTINGS2; SCALE_V; SCALE_H; 82 | BORDERSIZE 5, 0, 5, 5; 83 | COLUMNS 2; 84 | SPACE 4, 4; 85 | 86 | STATICTEXT IDC_STATIC_MANUAL { NAME IDS_STATIC_MANUAL; CENTER_V; CENTER_H; } 87 | GROUP IDC_GROUP_MANUAL 88 | { 89 | NAME IDS_GROUP_MANUAL; ALIGN_TOP; SCALE_H; 90 | BORDERSIZE 0, 0, 0, 0; 91 | COLUMNS 3; 92 | SPACE 4, 4; 93 | 94 | EDITNUMBERARROWS IDC_EDIT_X 95 | { CENTER_V; SCALE_H; SIZE 70, 0; } 96 | EDITNUMBERARROWS IDC_EDIT_Y 97 | { CENTER_V; SCALE_H; SIZE 70, 0; } 98 | EDITNUMBERARROWS IDC_EDIT_Z 99 | { CENTER_V; SCALE_H; SIZE 70, 0; } 100 | } 101 | } 102 | SEPARATOR { SCALE_H; } 103 | GROUP IDC_GROUP_BUTTONS 104 | { 105 | NAME IDS_GROUP_BUTTONS; ALIGN_TOP; CENTER_H; 106 | BORDERSIZE 0, 0, 0, 0; 107 | COLUMNS 2; 108 | SPACE 4, 4; 109 | 110 | BUTTON IDC_BUTTON_CANCEL { NAME IDS_BUTTON_CANCEL; ALIGN_TOP; CENTER_H; } 111 | BUTTON IDC_BUTTON_DOIT { NAME IDS_BUTTON_DOIT; ALIGN_TOP; CENTER_H; } 112 | } 113 | GROUP IDC_DUMMY 114 | { 115 | NAME IDS_DUMMY; ALIGN_TOP; SCALE_H; 116 | BORDERSIZE 0, 0, 0, 0; 117 | COLUMNS 1; 118 | SPACE 4, 4; 119 | 120 | 121 | } 122 | } 123 | } -------------------------------------------------------------------------------- /docs/pydocs3theme/static/pygments.css_t: -------------------------------------------------------------------------------- 1 | .highlight .hll { background-color: #ffffcc } 2 | .highlight { background: #eeffcc; } 3 | .highlight .c { color: #408090; font-style: italic } /* Comment */ 4 | .highlight .err { border: 1px solid #FF0000 } /* Error */ 5 | .highlight .k { color: #007020; font-weight: bold } /* Keyword */ 6 | .highlight .o { color: #666666 } /* Operator */ 7 | .highlight .cm { color: #408090; font-style: italic } /* Comment.Multiline */ 8 | .highlight .cp { color: #007020 } /* Comment.Preproc */ 9 | .highlight .c1 { color: #408090; font-style: italic } /* Comment.Single */ 10 | .highlight .cs { color: #408090; background-color: #fff0f0 } /* Comment.Special */ 11 | .highlight .gd { color: #A00000 } /* Generic.Deleted */ 12 | .highlight .ge { font-style: italic } /* Generic.Emph */ 13 | .highlight .gr { color: #FF0000 } /* Generic.Error */ 14 | .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 15 | .highlight .gi { color: #00A000 } /* Generic.Inserted */ 16 | .highlight .go { color: #333333 } /* Generic.Output */ 17 | .highlight .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ 18 | .highlight .gs { font-weight: bold } /* Generic.Strong */ 19 | .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 20 | .highlight .gt { color: #0044DD } /* Generic.Traceback */ 21 | .highlight .kc { color: #007020; font-weight: bold } /* Keyword.Constant */ 22 | .highlight .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */ 23 | .highlight .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */ 24 | .highlight .kp { color: #007020 } /* Keyword.Pseudo */ 25 | .highlight .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */ 26 | .highlight .kt { color: #902000 } /* Keyword.Type */ 27 | .highlight .m { color: #208050 } /* Literal.Number */ 28 | .highlight .s { color: #4070a0 } /* Literal.String */ 29 | .highlight .na { color: #4070a0 } /* Name.Attribute */ 30 | .highlight .nb { color: #007020 } /* Name.Builtin */ 31 | .highlight .nc { color: #0e84b5; font-weight: bold } /* Name.Class */ 32 | .highlight .no { color: #60add5 } /* Name.Constant */ 33 | .highlight .nd { color: #555555; font-weight: bold } /* Name.Decorator */ 34 | .highlight .ni { color: #d55537; font-weight: bold } /* Name.Entity */ 35 | .highlight .ne { color: #007020 } /* Name.Exception */ 36 | .highlight .nf { color: #06287e } /* Name.Function */ 37 | .highlight .nl { color: #002070; font-weight: bold } /* Name.Label */ 38 | .highlight .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ 39 | .highlight .nt { color: #062873; font-weight: bold } /* Name.Tag */ 40 | .highlight .nv { color: #bb60d5 } /* Name.Variable */ 41 | .highlight .ow { color: #007020; font-weight: bold } /* Operator.Word */ 42 | .highlight .w { color: #bbbbbb } /* Text.Whitespace */ 43 | .highlight .mf { color: #208050 } /* Literal.Number.Float */ 44 | .highlight .mh { color: #208050 } /* Literal.Number.Hex */ 45 | .highlight .mi { color: #208050 } /* Literal.Number.Integer */ 46 | .highlight .mo { color: #208050 } /* Literal.Number.Oct */ 47 | .highlight .sb { color: #4070a0 } /* Literal.String.Backtick */ 48 | .highlight .sc { color: #4070a0 } /* Literal.String.Char */ 49 | .highlight .sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */ 50 | .highlight .s2 { color: #4070a0 } /* Literal.String.Double */ 51 | .highlight .se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */ 52 | .highlight .sh { color: #4070a0 } /* Literal.String.Heredoc */ 53 | .highlight .si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */ 54 | .highlight .sx { color: #c65d09 } /* Literal.String.Other */ 55 | .highlight .sr { color: #235388 } /* Literal.String.Regex */ 56 | .highlight .s1 { color: #4070a0 } /* Literal.String.Single */ 57 | .highlight .ss { color: #517918 } /* Literal.String.Symbol */ 58 | .highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */ 59 | .highlight .vc { color: #bb60d5 } /* Name.Variable.Class */ 60 | .highlight .vg { color: #bb60d5 } /* Name.Variable.Global */ 61 | .highlight .vi { color: #bb60d5 } /* Name.Variable.Instance */ 62 | .highlight .il { color: #208050 } /* Literal.Number.Integer.Long */ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | The `py4dlib` library is a collection of Python modules to be used 5 | with MAXON's [CINEMA 4D](http://www.maxon.net "CINEMA 4D") 3D application. 6 | 7 | It's goal is to provide essentials but also convenience functions 8 | left out by CINEMA 4D's current Python SDK so that repetition and 9 | code duplication can be avoided. 10 | 11 | It currently includes routines for working with c4d objects, 12 | polygonal modelling tasks, some utilities and tools for writing 13 | plugins plus a small library of math routines needed for 3D work. 14 | 15 | You can view the current [documentation](http://andreberg.github.io/py4dlib "py4dlib docs") online. 16 | 17 | Better yet, you can watch the [introductional video](http://vimeo.com/72108443 "Introduction to py4dlib") on Vimeo! 18 | 19 | 20 | Installation 21 | ============ 22 | 23 | Take the `py4dlib` folder which you can find under `/source` in 24 | this repository, and put it under the following path: 25 | 26 | /library/python/packages/ 27 | 28 | `` is the prefs directory for CINEMA 4D in your home 29 | directory and `` is the name of the operating system you are 30 | running. 31 | 32 | For example on OS X this path could be: 33 | 34 | /Users//Library/Preferences/MAXON/CINEMA 4D R/library/python/packages/osx 35 | 36 | On Windows this path could be: 37 | 38 | %APPDATA%\MAXON\CINEMA 4D R\library\python\packages\win64 39 | 40 | 41 | Usage 42 | ===== 43 | 44 | Simply import any modules you'd like to use in your scripts or plugins, 45 | like you normally would: 46 | 47 | import py4dlib 48 | from py4dlib.objects import ObjectIterator 49 | 50 | To see if everything is working correctly you can try running an example 51 | script. 52 | 53 | Make a new scene, create a sphere and make it editable, then select it in 54 | the Object Manager. 55 | 56 | Open the Python script editor and run the following script: 57 | 58 | import c4d 59 | from c4d import gui 60 | #Welcome to the world of Python 61 | 62 | from py4dlib.utils import ClearConsole 63 | from py4dlib.examples import ShowPolygonNumber 64 | 65 | 66 | if __name__=='__main__': 67 | ClearConsole() 68 | doc = c4d.documents.GetActiveDocument() 69 | ShowPolygonNumber.main(doc) 70 | 71 | 72 | The result should be something like this: 73 | 74 | ![result](http://andreberg.github.io/py4dlib/_images/ShowPolygonNumber.png "Show Polygon Number") 75 | 76 | 77 | See also the [Best Practices](http://andreberg.github.io/py4dlib/usage.html#best-practices "Best Practices") section in the online docs. 78 | 79 | 80 | Contributions 81 | ============= 82 | 83 | I'd be very happy to accept code contributions. If you'd like to contribute 84 | please fork this repository, commit your changes to your local fork and then 85 | send me a pull request. 86 | 87 | If that’s too much of a hassle, you can also go ahead and create a gist or a 88 | pastebin post (or whatever copypasta service you fancy) and send me the URL so 89 | that I can include the code manually. 90 | 91 | 92 | Acknowledgements 93 | ================ 94 | 95 | a.k.a. **The Hall of Fame** 96 | 97 | In no particular order. 98 | 99 | Lennart Wåhlin (TCA Studios) 100 | Per-Anders Edwards 101 | Adam Swaab 102 | NitroMan (Nitro4D) 103 | Robert Leger 104 | Niklas Rosenstein 105 | Scott Ayers 106 | 107 | Thanks for making some of your C.O.F.F.E.E. and Python scripts 108 | free and for often not denying the ability to take a look. 109 | 110 | 111 | License 112 | ======= 113 | 114 | Licensed under the Apache License, Version 2.0 (the "License"); 115 | you may not use this file except in compliance with the License. 116 | You may obtain a copy of the License at 117 | 118 | http://www.apache.org/licenses/LICENSE-2.0 119 | 120 | Unless required by applicable law or agreed to in writing, software 121 | distributed under the License is distributed on an "AS IS" BASIS, 122 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 123 | See the License for the specific language governing permissions and 124 | limitations under the License. 125 | -------------------------------------------------------------------------------- /source/test/plugins_tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # plugins_tests.py 4 | # py4dlib 5 | # 6 | # Created by André Berg on 2012-09-26. 7 | # Copyright 2012 Berg Media. All rights reserved. 8 | # 9 | # andre.bergmedia@googlemail.com 10 | # 11 | # pylint: disable-msg=F0401 12 | 13 | import os 14 | import unittest 15 | 16 | __version__ = (0, 1) 17 | __date__ = '2012-09-26' 18 | __updated__ = '2013-08-01' 19 | 20 | from py4dlib.plugins import UserDefaults 21 | 22 | DEFAULTS = { 23 | 'triangulate': True, 24 | 'mode': 'quads', 25 | 'mindistance': 0.005 26 | } 27 | 28 | FILEPATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "res", "settings.ini") 29 | 30 | class PluginsTest(unittest.TestCase): 31 | 32 | def setUp(self): 33 | for apath in [FILEPATH]: 34 | if os.path.exists(apath) and not os.path.isdir(apath): 35 | print("removing %r" % apath) 36 | os.remove(apath) 37 | 38 | def tearDown(self): 39 | pass 40 | 41 | def testUserDefaultsCreationWithDefaults(self): 42 | settings = UserDefaults(filepath=FILEPATH, defaults=DEFAULTS) 43 | self.assertTrue(os.path.exists(settings.filepath)) 44 | statedefaults = settings.state.defaults() 45 | self.assertDictEqual(statedefaults, DEFAULTS, 'state defaults should be %r, but is %r' % (DEFAULTS, statedefaults)) 46 | expected_contents = """[DEFAULT] 47 | triangulate = True 48 | mindistance = 0.005 49 | mode = quads 50 | 51 | [Settings] 52 | 53 | """ 54 | with open(FILEPATH, 'rb') as savedconfig: 55 | actual_contents = savedconfig.read() 56 | self.assertEqual(expected_contents, actual_contents, 'expected contents should be %r, but is %r' % (expected_contents, actual_contents)) 57 | 58 | def testUserDefaultsCreationNoDefaults(self): 59 | settings = UserDefaults(filepath=FILEPATH) 60 | self.assertTrue(os.path.exists(settings.filepath)) 61 | statedefaults = settings.state.defaults() 62 | self.assertDictEqual(statedefaults, {}, 'state defaults should be %r, but is %r' % ({}, statedefaults)) 63 | 64 | def testUserDefaultsModificationAndSaving(self): 65 | settings = UserDefaults(filepath=FILEPATH) 66 | self.assertTrue(os.path.exists(settings.filepath)) 67 | settings.Set('string', 'some string') 68 | settings.Set('int', 1) 69 | settings.Set('float', 2.0) 70 | settings.Save() 71 | expected_contents = """[Settings] 72 | string = some string 73 | int = 1 74 | float = 2.0 75 | 76 | """ 77 | with open(FILEPATH, 'rb') as savedconfig: 78 | actual_contents = savedconfig.read() 79 | self.assertEqual(expected_contents, actual_contents, 'expected contents should be %r, but is %r' % (expected_contents, actual_contents)) 80 | 81 | def testUserDefaultsReadingAndRetrieving(self): 82 | settings = UserDefaults(filepath=FILEPATH) 83 | settings.Set('string', 'some string') 84 | settings.Set('int', 1) 85 | settings.Set('float', 2.0) 86 | settings.Save() 87 | settings2 = UserDefaults(filepath=FILEPATH) 88 | self.assertTrue(os.path.exists(settings2.filepath)) 89 | strval = settings2.Get('string') 90 | intval = settings2.Get('int') 91 | floatval = settings2.Get('float') 92 | expected_strval = 'some string' 93 | expected_intval = '1' 94 | expected_floatval = '2.0' 95 | self.assertEqual(strval, expected_strval, 'expected string value should be %r, but is %r' % (expected_strval, strval)) 96 | self.assertEqual(intval, expected_intval, 'expected int value should be %r, but is %r' % (expected_intval, intval)) 97 | self.assertEqual(floatval, expected_floatval, 'expected float value should be %r, but is %r' % (expected_floatval, floatval)) 98 | self.assertNotEqual(floatval, 2.0, 'expected float value should not be %r, but is %r' % (2.0, floatval)) 99 | 100 | 101 | if __name__ == "__main__": 102 | #import sys;sys.argv = ['', 'Test.testName'] 103 | unittest.main() 104 | 105 | 106 | # Licensed under the Apache License, Version 2.0 (the "License"); 107 | # you may not use this file except in compliance with the License. 108 | # You may obtain a copy of the License at 109 | # 110 | # http://www.apache.org/licenses/LICENSE-2.0 111 | # 112 | # Unless required by applicable law or agreed to in writing, software 113 | # distributed under the License is distributed on an "AS IS" BASIS, 114 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 115 | # See the License for the specific language governing permissions and 116 | # limitations under the License. 117 | -------------------------------------------------------------------------------- /docs/api/plugins.rst: -------------------------------------------------------------------------------- 1 | Plugins 2 | ------- 3 | 4 | 5 | .. class:: UserDefaults(filepath, defaults=None, header='Settings') 6 | 7 | Support for reading and writing settings files 8 | in .ini format. 9 | 10 | This can be used to provide state persistence 11 | for UI elements of a plugin. 12 | 13 | Examples: 14 | 15 | Initialize a new config object, modify and 16 | then save it: 17 | 18 | .. code:: 19 | 20 | >>> filepath = os.path.join(os.path.dirname(os.path.abspath(__file__)), "res", "settings.ini") 21 | >>> defaults = UserDefaults(filepath=filepath) 22 | >>> defaults.Set('key', 'value') 23 | >>> defaults.Save() 24 | 25 | If filepath points to an existing file, it will 26 | use that file and initializing a new config object 27 | by reading from it. 28 | 29 | If you later want to read in the config file: 30 | 31 | .. code:: 32 | 33 | >>> defaults.Read() 34 | >>> print(defaults.Get('setting')) 35 | value 36 | >>> print(defaults.Get('does-not-exist', default='use default instead')) 37 | use default instead 38 | 39 | :param str filepath: usually ``res/settings.ini`` relative to the 40 | source code file of the plugin, that uses the config store. 41 | :param defaults: ``dict`` default values to be used if the config 42 | file needs to be created. It is also possible to pass None 43 | here and then use the :py:func:`Set` and :py:func:`Save` functions 44 | to set inidividual settings after creation of the config object. 45 | :param str header: the name for a section in the .ini file. 46 | Usually you can get away with leaving it at the default. 47 | This will add a header ``[Settings]`` under which your 48 | settings will appear. If you have more advanced uses 49 | you are advised to modify the config parser state 50 | directly through ``self.state``. 51 | 52 | .. function:: Get(self, name, section=None, default=None, valuetype="str") 53 | 54 | Retrieve a previously stored value from the config object. 55 | 56 | :param str name: name of the setting 57 | :param str section: the section name. ``self.header`` if None. 58 | :param any default: a default value to use in case name wasn't found. 59 | :param str valuetype: type of the value to get. can be one of ``[str, bool, int, float]``. 60 | :return: ``str`` on success, None or *default* on failure. 61 | this will always return a string even if the value was 62 | stored as another type previously. So the caller is 63 | responsible for the convertion to the wanted data type. 64 | 65 | .. function:: GetInt(self, name, section=None, default=None) 66 | 67 | Retrieve a previously stored integer value from the config object. 68 | 69 | :param str name: name of the setting 70 | :param str section: the section name. ``self.header`` if None. 71 | :param any default: a default value to use in case name wasn't found. 72 | :return: ``int`` on success, None or 'default' on failure. 73 | 74 | .. function:: GetFloat(self, name, section=None, default=None) 75 | 76 | Retrieve a previously stored float value from the config object. 77 | 78 | :param str name: name of the setting 79 | :param str section: the section name. ``self.header`` if None. 80 | :param any default: a default value to use in case name wasn't found. 81 | :return: ``float`` on success, None or 'default' on failure. 82 | 83 | .. function:: GetBool(self, name, section=None, default=None) 84 | 85 | Retrieve a previously stored boolean value from the config object. 86 | 87 | :param str name: name of the setting 88 | :param str section: the section name. ``self.header`` if None. 89 | :param any default: a default value to use in case name wasn't found. 90 | :return: ``bool`` on success, None or 'default' on failure. 91 | 92 | .. function:: Set(self, name, value, section=None) 93 | 94 | Store a value in the config object for later retrieval. 95 | 96 | :param str name: name of the setting 97 | :param any value: value to set. 98 | :param str section: the section name. ``self.header`` if None. 99 | :return: True if successful, False otherwise. 100 | 101 | .. function:: Read(self) 102 | 103 | Read state from configuration file. 104 | 105 | :return: True if successful. 106 | :raise OSError: if config file couldn't be read. 107 | 108 | .. function:: Save(self, config=None, filepath=None) 109 | 110 | Save settings to a configuration file. 111 | 112 | :param ConfigParser config: the config object to save. 113 | If None, uses ``self.config`` instead. 114 | :param str filepath: allows for specifying another path 115 | than ``self.filepath`` in order to save a copy 116 | of the config object. 117 | :return: True if successful, False otherwise. 118 | 119 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ----- 3 | 4 | Simply import any modules you'd like to use in your scripts or plugins, 5 | like you normally would:: 6 | 7 | import py4dlib 8 | from py4dlib.objects import ObjectIterator 9 | 10 | To see if everything is working correctly you can try running an example 11 | script. 12 | 13 | Make a new scene, create a sphere and make it editable, then select it in 14 | the Object Manager. 15 | 16 | Open the Python script editor and run the following script:: 17 | 18 | import c4d 19 | from c4d import gui 20 | #Welcome to the world of Python 21 | 22 | from py4dlib.utils import ClearConsole 23 | from py4dlib.examples import ShowPolygonNumber 24 | 25 | 26 | if __name__=='__main__': 27 | ClearConsole() 28 | doc = c4d.documents.GetActiveDocument() 29 | ShowPolygonNumber.main(doc) 30 | 31 | The result should be something like this: 32 | 33 | .. image:: img/ShowPolygonNumber.png 34 | :width: 422.25 px 35 | :height: 351 px 36 | :alt: ShowPolygonNumber 37 | 38 | 39 | Best Practices 40 | -------------- 41 | 42 | What can you do if your plugin depends on **py4dlib** but 43 | the end user doesn't have the library installed? 44 | 45 | Well, you could try downloading the library using Python 46 | itself, but I'd caution against it. Running a plugin 47 | shouldn't start a connection to some random website to 48 | download stuff on the end user's computer. 49 | 50 | No, there are better ways to deal with this issue. 51 | For example, you could: 52 | 53 | 1) kindly ask the user to download the library herself 54 | 2) include the library with your script or plugin 55 | 3) copy and paste the methods you are using into your own script or plugin 56 | 57 | Asking The User 58 | ~~~~~~~~~~~~~~~ 59 | 60 | In the first case a sensible thing you can do, is check for 61 | prerequisite dependencies at the time the plugin is actually 62 | executed and if dependencies are missing display a message to 63 | the user and politely ask to download the library. 64 | 65 | You can find a complete example in the :ref:`Extract` plugin 66 | included in the :ref:`py4dlib.examples`, but I shall give a 67 | brief overview here. 68 | 69 | At the top of your plugin set a constant indicating the state 70 | of the dependency prerequisite and wrap any py4dlib imports in 71 | a try/except block, catching ``ImportError``:: 72 | 73 | PY4DLIB_FOUND = False 74 | 75 | try: 76 | from py4dlib.objects import DeselectAll, Select, GetGlobalRotation 77 | # more dependency imports ... 78 | PY4DLIB_FOUND = True 79 | except ImportError: 80 | pass 81 | 82 | Now, when the time comes to execute the plugin and do the bulk 83 | of the actual work, check if your prerequisite constant is True 84 | and if not display a message dialog, for example, like so:: 85 | 86 | PY4DLIB_NOT_FOUND_MSG = """This plugin needs py4dlib which is missing. 87 | 88 | Please download and install it free of charge 89 | from http://github.com/andreberg/py4dlib. 90 | """ 91 | 92 | # ... 93 | 94 | def run(self): 95 | if not PY4DLIB_FOUND: 96 | c4d.gui.MessageDialog(PY4DLIB_NOT_FOUND_MSG) 97 | return False 98 | # ... 99 | 100 | Including The Library 101 | ~~~~~~~~~~~~~~~~~~~~~ 102 | 103 | In the second case you can put the py4dlib source folder into a subdirectory 104 | included in your plugin or script distribution. To import the library you 105 | can then modify ``sys.path`` at the top of your script before any py4dlib 106 | imports. For example if you have the following directory structure:: 107 | 108 | icons/ 109 | res/ 110 | lib/ 111 | Plugin.pyp 112 | 113 | you can put the ``py4dlib`` folder under ``lib/`` and then modify ``sys.path`` 114 | at the top of your plugin/script, like so:: 115 | 116 | import sys, os 117 | sys.path.insert(1, os.path.realpath('lib')) # use 1 not 0! 118 | 119 | Note that we are using insert here instead of append. You can also use 120 | append but in that case the user's version of py4dlib will be loaded 121 | before your included one if the user has py4dlib installed. 122 | 123 | In case you are developing a script and not a plugin, it is the same, really. 124 | It just means you have to distribute an actual folder structure where a simple 125 | file would have done. But if you want to include icons you'd have to do that 126 | anyway. 127 | 128 | Copy And Paste Methods 129 | ~~~~~~~~~~~~~~~~~~~~~~ 130 | 131 | If you don't want to make the user install anything and you also don't want to 132 | include the library, then you will have to copy and paste the py4dlib methods 133 | you are using into your plugin. 134 | 135 | In that case I recommend developing your plugin or script with the usual 136 | dependency imports until you have it just right. Then before you deliver 137 | the final version to your users, create a version with just the neccessary 138 | methods inlined. 139 | 140 | Special care was given to make the py4dlib API not too self-entangled so 141 | ripping stuff out to copy+paste it into your own scripts shouldn't prove 142 | too difficult. 143 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /docs/pydocs3theme/static/default.css_t: -------------------------------------------------------------------------------- 1 | /* 2 | * default.css_t 3 | * ~~~~~~~~~~~~~ 4 | * 5 | * Sphinx stylesheet -- default theme. 6 | * 7 | * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | 12 | @import url("basic.css"); 13 | 14 | /* -- page layout ----------------------------------------------------------- */ 15 | 16 | body { 17 | font-family: 'Lucida Grande', Arial, sans-serif; 18 | font-size: 100%; 19 | background-color: white; 20 | color: #000; 21 | margin: 0; 22 | padding: 0; 23 | } 24 | 25 | div.document { 26 | background-color: white; 27 | } 28 | 29 | div.documentwrapper { 30 | float: left; 31 | width: 100%; 32 | } 33 | 34 | div.bodywrapper { 35 | margin: 0 0 0 230px; 36 | } 37 | 38 | div.body { 39 | background-color: white; 40 | color: #222222; 41 | padding: 0 20px 30px 20px; 42 | } 43 | 44 | div.footer { 45 | color: #555555; 46 | width: 100%; 47 | padding: 9px 0 9px 0; 48 | text-align: center; 49 | font-size: 75%; 50 | } 51 | 52 | div.footer a { 53 | color: #555555; 54 | text-decoration: underline; 55 | } 56 | 57 | div.related { 58 | background-color: white; 59 | line-height: 30px; 60 | color: #666666; 61 | } 62 | 63 | div.related a { 64 | color: #444444; 65 | } 66 | 67 | div.sphinxsidebar { 68 | } 69 | 70 | div.sphinxsidebar h3 { 71 | font-family: 'Lucida Grande', Arial, sans-serif; 72 | color: #444444; 73 | font-size: 1.4em; 74 | font-weight: normal; 75 | margin: 0; 76 | padding: 0; 77 | } 78 | 79 | div.sphinxsidebar h3 a { 80 | color: #444444; 81 | } 82 | 83 | div.sphinxsidebar h4 { 84 | font-family: 'Lucida Grande', Arial, sans-serif; 85 | color: #444444; 86 | font-size: 1.3em; 87 | font-weight: normal; 88 | margin: 5px 0 0 0; 89 | padding: 0; 90 | } 91 | 92 | div.sphinxsidebar p { 93 | color: #444444; 94 | } 95 | 96 | div.sphinxsidebar p.topless { 97 | margin: 5px 10px 10px 10px; 98 | } 99 | 100 | div.sphinxsidebar ul { 101 | margin: 10px; 102 | padding: 0; 103 | color: #444444; 104 | } 105 | 106 | div.sphinxsidebar a { 107 | color: #444444; 108 | } 109 | 110 | div.sphinxsidebar input { 111 | border: 1px solid #444444; 112 | font-family: sans-serif; 113 | font-size: 1em; 114 | } 115 | 116 | 117 | /* for collapsible sidebar */ 118 | div#sidebarbutton { 119 | background-color: #3c6e83; 120 | } 121 | 122 | 123 | /* -- hyperlink styles ------------------------------------------------------ */ 124 | 125 | a { 126 | color: #0090c0; 127 | text-decoration: none; 128 | } 129 | 130 | a:visited { 131 | color: #00608f; 132 | text-decoration: none; 133 | } 134 | 135 | a:hover { 136 | text-decoration: underline; 137 | } 138 | 139 | 140 | 141 | /* -- body styles ----------------------------------------------------------- */ 142 | 143 | div.body h1, 144 | div.body h2, 145 | div.body h3, 146 | div.body h4, 147 | div.body h5, 148 | div.body h6 { 149 | font-family: 'Lucida Grande', Arial, sans-serif; 150 | background-color: white; 151 | font-weight: normal; 152 | color: #1a1a1a; 153 | border-bottom: 1px solid #ccc; 154 | margin: 20px -20px 10px -20px; 155 | padding: 3px 0 3px 10px; 156 | } 157 | 158 | div.body h1 { margin-top: 0; font-size: 200%; } 159 | div.body h2 { font-size: 160%; } 160 | div.body h3 { font-size: 140%; } 161 | div.body h4 { font-size: 120%; } 162 | div.body h5 { font-size: 110%; } 163 | div.body h6 { font-size: 100%; } 164 | 165 | a.headerlink { 166 | color: #aaaaaa; 167 | font-size: 0.8em; 168 | padding: 0 4px 0 4px; 169 | text-decoration: none; 170 | } 171 | 172 | a.headerlink:hover { 173 | background-color: #aaaaaa; 174 | color: white; 175 | } 176 | 177 | div.body p, div.body dd, div.body li { 178 | text-align: justify; 179 | line-height: 130%; 180 | } 181 | 182 | div.admonition p.admonition-title + p { 183 | display: inline; 184 | } 185 | 186 | div.admonition p { 187 | margin-bottom: 5px; 188 | } 189 | 190 | div.admonition pre { 191 | margin-bottom: 5px; 192 | } 193 | 194 | div.admonition ul, div.admonition ol { 195 | margin-bottom: 5px; 196 | } 197 | 198 | div.note { 199 | background-color: #eee; 200 | border: 1px solid #ccc; 201 | } 202 | 203 | div.seealso { 204 | background-color: #ffc; 205 | border: 1px solid #ff6; 206 | } 207 | 208 | div.topic { 209 | background-color: #eee; 210 | } 211 | 212 | div.warning { 213 | background-color: #ffe4e4; 214 | border: 1px solid #f66; 215 | } 216 | 217 | p.admonition-title { 218 | display: inline; 219 | } 220 | 221 | p.admonition-title:after { 222 | content: ":"; 223 | } 224 | 225 | pre { 226 | padding: 5px; 227 | background-color: #eeffcc; 228 | color: #333333; 229 | line-height: 120%; 230 | border: 1px solid #ac9; 231 | border-left: none; 232 | border-right: none; 233 | } 234 | 235 | tt { 236 | background-color: #ecf0f3; 237 | padding: 0 1px 0 1px; 238 | font-size: 0.95em; 239 | } 240 | 241 | th { 242 | background-color: #ede; 243 | } 244 | 245 | .warning tt { 246 | background: #efc2c2; 247 | } 248 | 249 | .note tt { 250 | background: #d6d6d6; 251 | } 252 | 253 | .viewcode-back { 254 | font-family: 'Lucida Grande', Arial, sans-serif; 255 | } 256 | 257 | div.viewcode-block:target { 258 | background-color: #f4debf; 259 | border-top: 1px solid #ac9; 260 | border-bottom: 1px solid #ac9; 261 | } -------------------------------------------------------------------------------- /source/test/objects/object_iterator_tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # object_iterator_tests.py 4 | # py4dlib 5 | # 6 | # Created by André Berg on 2012-09-26. 7 | # Copyright 2013 Berg Media. All rights reserved. 8 | # 9 | # This file is a copy of the Python tag script 10 | # found in the C4D file of the same name. 11 | # It is provided for reference if C4D is not 12 | # available or the .c4d file can't be opened. 13 | # 14 | # andre.bergmedia@googlemail.com 15 | # 16 | # pylint: disable-msg=F0401,W0611 17 | 18 | import os 19 | 20 | DEBUG = 1 or ('DebugLevel' in os.environ and os.environ['DebugLevel'] > 0) 21 | TESTRUN = 0 or ('TestRunLevel' in os.environ and os.environ['TestRunLevel'] > 0) 22 | 23 | try: 24 | from c4d import documents #@UnresolvedImport 25 | except ImportError: 26 | if TESTRUN == 1: 27 | pass 28 | 29 | from py4dlib import utils 30 | from py4dlib.objects import ObjectIterator 31 | 32 | 33 | def main(): 34 | utils.ClearConsole() 35 | 36 | doc = documents.GetActiveDocument() 37 | if not doc: 38 | return False 39 | 40 | # get some objects from the om tree... 41 | # ...at top level hierarchy 42 | group1obj = doc.SearchObject('Group1') 43 | # ... and somewhere further down 44 | cube2obj = doc.SearchObject('Cube2') 45 | if not group1obj or not cube2obj: 46 | return False 47 | 48 | indent = 4 49 | 50 | print("this should iterate through Group1's children and") 51 | print("it should stop before Group2\n") 52 | 53 | cur = [] 54 | for op, lvl in ObjectIterator(group1obj): 55 | print("%s%s" % (' ' * lvl * indent, op.GetName())) 56 | cur.append(op.GetName()) 57 | 58 | assert(cur == ['Group1A', 'Cube', 'Würfel', 'Group1AA', 'Cube2', 59 | 'Wuerfel2', 'Group1B', 'Sweep-NURBS', 'Kreis', 'Heart']) 60 | 61 | print("\n") 62 | print("this should iterate starting from and including Group1") 63 | print("and it should not stop unless at the end of the object") 64 | print("manager top level\n") 65 | 66 | cur = [] 67 | for op, lvl in ObjectIterator(group1obj, children_only=False): 68 | print("%s%s" % (' ' * lvl * indent, op.GetName())) 69 | cur.append(op.GetName()) 70 | 71 | assert(cur == ["Group1", "Group1A", "Cube", "Würfel", "Group1AA", 72 | "Cube2", "Wuerfel2", "Group1B", "Sweep-NURBS", "Kreis", "Heart", 73 | "Group2", "Röhre", "Zylinder", "Zylinder2", "Group3", "Group3A", 74 | "Group3AA", "Group3AB", "Kugel", "Platonischer Körper", "Group4", 75 | "Group4A", "Group4AA", "Group4AB", "Kugel2", "Platonischer Körper2"]) 76 | 77 | print("\n") 78 | print("this should iterate through Group1's children only and it ") 79 | print("should stop before Cube2:\n") 80 | 81 | cur = [] 82 | for op, lvl in ObjectIterator(group1obj, stop_obj=cube2obj): 83 | print("%s%s" % (' ' * lvl * indent, op.GetName())) 84 | cur.append(op.GetName()) 85 | 86 | assert(cur == ["Group1A", "Cube", "Würfel", "Group1AA"]) 87 | 88 | # now test iterating within a sub hierarchy 89 | 90 | # get some more objects from the om tree... 91 | # ...at sub level hierarchy 92 | group3obj = doc.SearchObject('Group3') 93 | # ... and somewhere further down 94 | group3abobj = doc.SearchObject('Group3AB') 95 | if not group3obj or not group3abobj: 96 | return False 97 | 98 | print("\n") 99 | print("this should iterate through Group3's children and it should stop") 100 | print("before going up again through Zylinder2\n") 101 | 102 | cur = [] 103 | for op, lvl in ObjectIterator(group3obj, startlvl=4): 104 | print("%s%s" % (' ' * lvl * indent, op.GetName())) 105 | cur.append(op.GetName()) 106 | 107 | assert(cur == ["Group3A", "Group3AA", "Group3AB", "Kugel", "Platonischer Körper"]) 108 | 109 | print("\n") 110 | print("this should iterate starting from and including Group3 and it ") 111 | print("should not stop unless at the end of the object manager top level\n") 112 | 113 | cur = [] 114 | for op, lvl in ObjectIterator(group3obj, children_only=False, startlvl=4): 115 | print("%s%s" % (' ' * lvl * indent, op.GetName())) 116 | cur.append(op.GetName()) 117 | 118 | assert(cur == ["Group3", "Group3A", "Group3AA", "Group3AB", "Kugel", 119 | "Platonischer Körper", "Group4", "Group4A", "Group4AA", "Group4AB", 120 | "Kugel2", "Platonischer Körper2"]) 121 | 122 | print("\n") 123 | print("this should iterate through Group3's children only and it should ") 124 | print("stop after Group3AB\n") 125 | 126 | cur = [] 127 | for op, lvl in ObjectIterator(group3obj, stop_obj=group3abobj, startlvl=4): 128 | print("%s%s" % (' ' * lvl * indent, op.GetName())) 129 | cur.append(op.GetName()) 130 | 131 | assert(cur == ["Group3A", "Group3AA"]) 132 | 133 | 134 | if __name__ == '__main__': 135 | main() 136 | 137 | 138 | # Licensed under the Apache License, Version 2.0 (the "License"); 139 | # you may not use this file except in compliance with the License. 140 | # You may obtain a copy of the License at 141 | # 142 | # http://www.apache.org/licenses/LICENSE-2.0 143 | # 144 | # Unless required by applicable law or agreed to in writing, software 145 | # distributed under the License is distributed on an "AS IS" BASIS, 146 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 147 | # See the License for the specific language governing permissions and 148 | # limitations under the License. 149 | -------------------------------------------------------------------------------- /source/test/maths_tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # test.maths_tests 4 | # project 5 | # 6 | # Created by André Berg on 2013-08-02. 7 | # Copyright 2013 Berg Media. All rights reserved. 8 | # 9 | # andre.bergmedia@googlemail.com 10 | # 11 | # pylint: disable-msg=F0401 12 | 13 | import os 14 | import math 15 | import unittest 16 | 17 | __version__ = (0, 1) 18 | __date__ = '2013-08-02' 19 | __updated__ = '2013-08-05' 20 | 21 | 22 | DEBUG = 1 or ('DebugLevel' in os.environ and os.environ['DebugLevel'] > 0) 23 | TESTRUN = 0 or ('TestRunLevel' in os.environ and os.environ['TestRunLevel'] > 0) 24 | 25 | 26 | try: 27 | import c4d #@UnresolvedImport 28 | except ImportError: 29 | if TESTRUN == 1: 30 | pass 31 | 32 | from py4dlib.maths import Det, UnitNormal, Transpose, VLerp, VNLerp, VSLerp 33 | 34 | eps = 0.000001 35 | 36 | def FloatEqual(a, b, places=len(str(eps))-2): 37 | return round(abs(b - a), places) == 0 38 | 39 | class VectorMock(object): 40 | """ Mock object for c4d.Vectors """ 41 | 42 | def __init__(self, x, y=None, z=None): 43 | if y is None: 44 | y = x 45 | if z is None: 46 | z = x 47 | self.x = float(x) 48 | self.y = float(y) 49 | self.z = float(z) 50 | 51 | def __str__(self): 52 | return ("%r, x = %s, y = %s, z = %s" % (self, self.x, self.y, self.z)) 53 | 54 | def __len__(self): 55 | return self.GetLength() 56 | 57 | def __sub__(self, other): 58 | return VectorMock(self.x - other.x, self.y - other.y, self.z - other.z) 59 | 60 | def __rsub__(self, other): 61 | return VectorMock(other.x - self.x, other.y - self.y, other.z - self.z) 62 | 63 | def __add__(self, other): 64 | return VectorMock(self.x + other.x, self.y + other.y, self.z + other.z) 65 | 66 | def __mul__(self, other): 67 | return VectorMock(self.x * other, self.y * other, self.z * other) 68 | 69 | def __rmul__(self, other): 70 | return self.__mul__(other) 71 | 72 | def __eq__(self, other): 73 | return (FloatEqual(self.x, other.x) and FloatEqual(self.y, other.y) and FloatEqual(self.z, other.z)) 74 | 75 | def GetLength(self): 76 | return math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z) 77 | 78 | def GetNormalized(self): 79 | ool = 1.0 / len(self) 80 | return VectorMock(self.x * ool, self.y * ool, self.z * ool) 81 | 82 | def Normalize(self): 83 | ool = 1.0 / len(self) 84 | self.x *= ool 85 | self.y *= ool 86 | self.z *= ool 87 | 88 | def Dot(self, other): 89 | return float(self.x * other.x + self.y * other.y + self.z * other.z) 90 | 91 | def Cross(self, other): 92 | return VectorMock(self.y * other.z - self.z * other.y, 93 | self.z * other.x - self.x * other.z, 94 | self.x * other.y - self.y * other.x) 95 | 96 | class CPolygonMock(object): 97 | """ Mock object for c4d.CPolygons """ 98 | def __init__(self, a, b, c, d=None): 99 | self.a = a 100 | self.b = b 101 | self.c = c 102 | if d is None: 103 | d = c 104 | self.d = d 105 | 106 | 107 | class Test(unittest.TestCase): 108 | 109 | 110 | def testDet(self): 111 | m = [[ 1, 22, 3], 112 | [ 5, 1, 7], 113 | [ 9, -1, 2]] 114 | det = Det(m) 115 | 116 | self.assertEquals(1133, det) 117 | 118 | m2 = [[ 1, 22, 3, 4], 119 | [ 5, 6, 7, 8], 120 | [ 9, 22, 22, 12], 121 | [13, 14, 15, 16]] 122 | det2 = Det(m2) 123 | 124 | self.assertEquals(5280, det2) 125 | 126 | def testUnitNormal(self): 127 | a = VectorMock(-100, 100, 100) 128 | b = VectorMock(-100, 100, -100) 129 | c = VectorMock(-100, -100, 100) 130 | 131 | normal = UnitNormal(a, b, c) 132 | 133 | self.assertEquals(-1.0, normal.x) 134 | 135 | def testTranspose(self): 136 | m44 = [[1, 2, 3, 4], 137 | [5, 6, 7, 8], 138 | [9, 10, 11, 12], 139 | [13, 14, 15, 16]] 140 | 141 | expected = [[1, 5, 9, 13], 142 | [2, 6, 10, 14], 143 | [3, 7, 11, 15], 144 | [4, 8, 12, 16]] 145 | 146 | m44_t = Transpose(m44) 147 | self.assertEquals(m44_t, expected) 148 | 149 | def testVLerp(self): 150 | vs = VectorMock(2, 2, 2) 151 | ve = VectorMock(4, 4, 4) 152 | 153 | vl = VLerp(vs, ve) 154 | expected = VectorMock(3.0) 155 | 156 | self.assertEquals(vl, expected) 157 | 158 | def testVNLerp(self): 159 | vs = VectorMock(2, 2, 2) 160 | ve = VectorMock(4, 4, 4) 161 | 162 | vnl = VNLerp(vs, ve) 163 | expected = VectorMock(0.6) 164 | 165 | self.assertEquals(vnl, expected) 166 | print(vnl) 167 | 168 | 169 | def testSLerp(self): 170 | vs = VectorMock(0, 1, 0) 171 | ve = VectorMock(1, 0, 0) 172 | 173 | vsl = VSLerp(vs, ve) 174 | expected = VectorMock(0.707106781187, 0.707106781187, 0.0) 175 | 176 | self.assertEquals(vsl, expected) 177 | print(vsl) 178 | 179 | if __name__ == "__main__": 180 | #import sys;sys.argv = ['', 'Test.testName'] 181 | unittest.main() 182 | 183 | 184 | # Licensed under the Apache License, Version 2.0 (the "License"); 185 | # you may not use this file except in compliance with the License. 186 | # You may obtain a copy of the License at 187 | # 188 | # http://www.apache.org/licenses/LICENSE-2.0 189 | # 190 | # Unless required by applicable law or agreed to in writing, software 191 | # distributed under the License is distributed on an "AS IS" BASIS, 192 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 193 | # See the License for the specific language governing permissions and 194 | # limitations under the License. 195 | -------------------------------------------------------------------------------- /source/test/objects_tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # objects_tests.py 4 | # py4dlib 5 | # 6 | # Created by André Berg on 2012-09-28. 7 | # Copyright 2012 Berg Media. All rights reserved. 8 | # 9 | # andre.bergmedia@googlemail.com 10 | # 11 | # pylint: disable-msg=F0401,W1401 12 | 13 | import os 14 | import unittest 15 | import pprint 16 | 17 | __version__ = (0, 1) 18 | __date__ = '2012-09-28' 19 | __updated__ = '2013-08-08' 20 | 21 | 22 | from py4dlib.objects import ObjectHierarchy 23 | 24 | 25 | pp = pprint.PrettyPrinter() 26 | PP = pp.pprint 27 | PF = pp.pformat 28 | 29 | DEBUG = 1 or ('DebugLevel' in os.environ and os.environ['DebugLevel'] > 0) 30 | TESTRUN = 0 or ('TestRunLevel' in os.environ and os.environ['TestRunLevel'] > 0) 31 | 32 | 33 | DATA = { 34 | 'Source/Group1ABB': [ 35 | 'Kegel1', 36 | 'Kegel2', 37 | 'Polygons1' 38 | ], 'Target/Group1/Group1A/Group1AB': [ 39 | 'Cube3', 40 | 'W\ürfel3', 41 | 'Group1ABA', 42 | 'Group1ABB' 43 | ], 'Target': [ 44 | 'Target', 45 | 'Group1' 46 | ], 'Target/Group1/Group1A/Group1AA': [ 47 | 'Cube2', 48 | 'W\ürfel2' 49 | ], 'Source/Group1ABA/Sweep-NURBS': [ 50 | 'Kreis', 51 | 'Heart' 52 | ], 'Target/Group1/Group1A': [ 53 | 'Cube1', 54 | 'W\ürfel1', 55 | 'Group1AA', 56 | 'Group1AB', 57 | 'Group1AC' 58 | ], 'Source/Group1AC': [ 59 | 'Cube3', 60 | 'W\ürfel3' 61 | ], 'Source/Group1AA': [ 62 | 'Cube2', 'W\ürfel2' 63 | ], 'Source/Group1AB': [ 64 | 'Cube3', 65 | 'W\ürfel3' 66 | ], 'Target/Group1/Group1A/Group1AC': [ 67 | 'Cube3', 68 | 'W\ürfel3', 69 | 'Group1ACA' 70 | ], 'Source': [ 71 | 'Source', 72 | 'Group1', 73 | 'Group1A', 74 | 'Group1AA', 75 | 'Group1AB', 76 | 'Group1ABA', 77 | 'Group1ABB', 78 | 'Group1AC', 79 | 'Group1ACA' 80 | ], 'Source/Group1ABA': [ 81 | 'Sweep-NURBS' 82 | ], 'Source/Group1A': [ 83 | 'Cube1', 84 | 'W\ürfel1' 85 | ], 'Target/Group1': [ 86 | 'Group1A' 87 | ], 'Target/Group1/Group1A/Group1AB/Group1ABA/Sweep-NURBS': [ 88 | 'Kreis', 89 | 'Heart' 90 | ], 'Target/Group1/Group1A/Group1AB/Group1ABB': [ 91 | 'Kegel1', 92 | 'Kegel2', 93 | 'Polygons1' 94 | ], 'Target/Group1/Group1A/Group1AB/Group1ABA': [ 95 | 'Sweep-NURBS' 96 | ] 97 | } 98 | 99 | class OHMock(ObjectHierarchy): 100 | '''ObjectHierarchy mock object''' 101 | def __init__(self, *args, **kwargs): # IGNORE:W0231 102 | # yes, we don't call/init super here to avoid 'c4d' is not defined 103 | self.sep = '/' 104 | self.entries = DATA 105 | 106 | 107 | class ObjectsTest(unittest.TestCase): 108 | 109 | def setUp(self): 110 | self.mockobj = OHMock() 111 | 112 | def testGetFullyQualifiedKeyPath(self): 113 | expected = ['Sweep-NURBS'] 114 | path = 'Target/Group1/Group1A/Group1AB/Group1ABA' 115 | actual = self.mockobj.Get(path) 116 | self.assertEqual(actual, expected, 'actual should equal %r, but is %r' % (expected, actual)) 117 | 118 | def testGetWildcardExpandedKeyPath1(self): 119 | expected = ['Kreis', 'Heart', 'Sweep-NURBS', 'Kegel1', 'Kegel2', 'Polygons1'] 120 | path = 'Target/Group1/Group1A/Group1AB/*' 121 | actual = self.mockobj.Get(path) 122 | self.assertEqual(actual, expected, 'actual should equal %r, but is %r' % (expected, actual)) 123 | 124 | def testGetWildcardExpandedKeyPath2(self): 125 | expected = ['Kreis', 'Heart', 'Sweep-NURBS', 'Kegel1', 'Kegel2', 'Polygons1'] 126 | path = 'Target/*/*/*/*' 127 | actual = self.mockobj.Get(path) 128 | self.assertEqual(actual, expected, 'actual should equal %r, but is %r' % (expected, actual)) 129 | 130 | def testGetWildcardExpandedKeyPath3(self): 131 | # this also matches 'Target/Group1/Group1A/Group1AB' directly now 132 | # since the wildcard character is not after the sepaerator and thus 133 | # can evaluate to nothing 134 | expected = ['Kreis', 'Heart', 'Cube3', 'W\ürfel3', 'Group1ABA', 'Group1ABB', 'Sweep-NURBS', 'Kegel1', 'Kegel2', 'Polygons1'] 135 | path = 'Target/Group1/Group1A/Group1AB*' 136 | actual = self.mockobj.Get(path) 137 | self.assertEqual(actual, expected, 'actual should equal %r, but is %r' % (expected, actual)) 138 | 139 | def testGetPatternExpandedKeyPath(self): 140 | expected = ['Target', 'Group1', 'Kegel1', 'Kegel2', 'Polygons1'] 141 | path = '(Source/Group1ABB|Target)' 142 | actual = self.mockobj.Get(path) 143 | self.assertEqual(actual, expected, 'actual should equal %r, but is %r' % (expected, actual)) 144 | 145 | def testGetRePatternExpandedKeyPath(self): 146 | expected = ['Kegel1', 'Kegel2', 'Polygons1'] 147 | path = '!(?!\T)(Source/Group1ABB|Target)' 148 | actual = self.mockobj.Get(path) 149 | self.assertEqual(actual, expected, 'actual should equal %r, but is %r' % (expected, actual)) 150 | 151 | def testGetRelativeExpandedKeyPath(self): 152 | expected = ['Cube3', 'W\ürfel3', 'Group1ABA', 'Group1ABB'] 153 | path = 'Target/Group1/Group1A/Group1AB/Group1ABA/..' 154 | actual = self.mockobj.Get(path) 155 | self.assertEqual(actual, expected, 'actual should equal %r, but is %r' % (expected, actual)) 156 | 157 | 158 | if __name__ == "__main__": 159 | #import sys;sys.argv = ['', 'Test.testName'] 160 | unittest.main() 161 | 162 | 163 | # Licensed under the Apache License, Version 2.0 (the "License"); 164 | # you may not use this file except in compliance with the License. 165 | # You may obtain a copy of the License at 166 | # 167 | # http://www.apache.org/licenses/LICENSE-2.0 168 | # 169 | # Unless required by applicable law or agreed to in writing, software 170 | # distributed under the License is distributed on an "AS IS" BASIS, 171 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 172 | # See the License for the specific language governing permissions and 173 | # limitations under the License. -------------------------------------------------------------------------------- /source/py4dlib/examples/CreatePlanesFromPolygons.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # CreatePlanesFromPolygons.py 4 | # py4dlib.examples 5 | # 6 | # Created by André Berg on 2013-08-08. 7 | # Copyright 2013 Berg Media. All rights reserved. 8 | # 9 | # andre.bergmedia@googlemail.com 10 | # 11 | # CreatePlanesFromPolygons 12 | # 13 | # Create a workplane or plane from a selected polygon. 14 | # 15 | # Summary: 16 | # 17 | # Example script that shows how to create a planar primitive such as a workplane 18 | # or a plane object and adjust its orientation to match the modelling axis set to 19 | # normal mode. 20 | # 21 | # pylint: disable-msg=F0401 22 | 23 | """ 24 | Name-US:Create Planes From Polygons 25 | Description-US:Create plane or workingplane objects for each selected polygon on a group of selected objects. 26 | """ 27 | 28 | import os 29 | 30 | __version__ = (0, 2) 31 | __date__ = '2013-08-08' 32 | __updated__ = '2013-08-08' 33 | 34 | 35 | DEBUG = 0 or ('DebugLevel' in os.environ and os.environ['DebugLevel'] > 0) 36 | TESTRUN = 0 or ('TestRunLevel' in os.environ and os.environ['TestRunLevel'] > 0) 37 | 38 | try: 39 | import c4d #@UnresolvedImport 40 | from c4d import documents 41 | except ImportError: 42 | if TESTRUN == 1: 43 | pass 44 | 45 | 46 | # R14 and above no longer have a workingplane object 47 | # and I don't know yet how to manipulate the document 48 | # workingplane mode. 49 | if c4d.GetC4DVersion() > 13999: 50 | TYPE = c4d.Oplane 51 | else: 52 | TYPE = c4d.Oplane 53 | 54 | 55 | from py4dlib.utils import ClearConsole 56 | from py4dlib.maths import BuildMatrix3, IsZeroVector 57 | from py4dlib.objects import CreateObject 58 | from py4dlib.mesh import CalcPolyCentroid, CalcPolyNormal 59 | 60 | 61 | def CreatePlanesFromPolygons(op, typ): 62 | if not isinstance(op, c4d.PolygonObject): 63 | raise TypeError("E: expected c4d.PolygonObject, got %r" % (type(op))) 64 | 65 | allpolys = op.GetAllPolygons() 66 | polysel = op.GetPolygonS() 67 | 68 | selpolys = [] 69 | for i, p in enumerate(allpolys): 70 | if polysel.IsSelected(i) == True: 71 | selpolys.append((i, p)) 72 | 73 | if len(selpolys) == 0: 74 | print("need at least 1 polygon selected for CreatePlaneFromPolygon") 75 | return False 76 | 77 | mg = op.GetMg() 78 | op_name = op.GetName() 79 | 80 | planes = [] 81 | 82 | for i, p in selpolys: 83 | 84 | pids = [p.a, p.b, p.c] 85 | 86 | p1 = p.a 87 | p2 = p.b 88 | p3 = p.c 89 | p4 = p.d 90 | 91 | plen = 3 92 | if p.c != p.d: 93 | pids.append(p4) 94 | plen = 4 95 | 96 | allp = op.GetAllPoints() 97 | 98 | pnts = [] 99 | for id_ in pids: 100 | pnts.append(allp[id_]) 101 | 102 | a = allp[p1] 103 | b = allp[p2] 104 | c = allp[p3] 105 | d = allp[p4] 106 | 107 | cv1 = a - b 108 | cv2 = b - c 109 | cv3 = c - d 110 | cv4 = d - a 111 | if plen == 4: 112 | cva = (cv3 - cv1) 113 | cvb = (cv4 - cv2) 114 | else: 115 | cva = cv3 - cv1 116 | cvb = cv3 - cv2 117 | 118 | if DEBUG: 119 | print("%d: a = %r, b = %r, c = %r, d = %r" % (i, a,b,c,d)) 120 | print("cv1 = %r" % cv1) 121 | print("cv2 = %r" % cv2) 122 | print("cv3 = %r" % cv3) 123 | print("cv4 = %r" % cv4) 124 | print("cva = %r" % cva) 125 | print("cvb = %r" % cvb) 126 | 127 | pn = CalcPolyNormal(p, op) 128 | pc = CalcPolyCentroid(pids, op) 129 | 130 | AXIS_ZY = 1 131 | 132 | base = "x" 133 | axis = AXIS_ZY 134 | 135 | if plen == 4: 136 | cv = c4d.Vector(cva.x, cvb.y/2, cva.z) 137 | else: 138 | if cvb.y == 0: 139 | cv = cvb 140 | else: 141 | cv = cva 142 | 143 | if IsZeroVector(cv): 144 | if cv == cva: 145 | cv = cvb 146 | else: 147 | cv = cva 148 | 149 | cv ^= mg 150 | 151 | if DEBUG: 152 | #print("vmax = %r" % vmax) 153 | print("pn = %r" % pn) 154 | print("cv = %r" % cv) 155 | print("base = %r" % base) 156 | 157 | new_mg = BuildMatrix3(pn ^ mg, cv, off=(pc * mg), base=base) 158 | #new_mg.v2 = -new_mg.v2 159 | 160 | name = "%s Plane %d" % (op_name, i) 161 | plane = CreateObject(typ, name) 162 | if plane is None: 163 | return False 164 | 165 | plane.SetMg(new_mg) 166 | 167 | if typ == c4d.Oconplane: 168 | plane[c4d.CONSTRUCTIONPLANE_TYPE] = axis 169 | plane[c4d.CONSTRUCTIONPLANE_SPACING] = 10 170 | elif typ == c4d.Oplane: 171 | plane[c4d.PRIM_AXIS] = axis 172 | plane[c4d.PRIM_PLANE_WIDTH] = 75 173 | plane[c4d.PRIM_PLANE_HEIGHT] = plane[c4d.PRIM_PLANE_WIDTH] 174 | else: 175 | if DEBUG: 176 | print("Unknown plane type: %r" % typ) 177 | return False 178 | 179 | plane.Message(c4d.MSG_UPDATE) 180 | planes.append(plane) 181 | 182 | return planes 183 | 184 | 185 | def main(): 186 | doc = documents.GetActiveDocument() 187 | if doc is None: 188 | return False 189 | 190 | doc.StartUndo() 191 | 192 | sel = doc.GetSelection() 193 | if sel is None: 194 | return False 195 | 196 | # loop through all objects 197 | for op in sel: 198 | if not isinstance(op, c4d.PolygonObject): 199 | if DEBUG: 200 | print("%s: not a polygon object. Skipping..." % str(op.GetName())) 201 | continue 202 | 203 | print("creating plane(s) for object: %s" % op.GetName()) 204 | 205 | CreatePlanesFromPolygons(op, TYPE) 206 | 207 | # tell C4D to update internal state 208 | c4d.EventAdd() 209 | doc.EndUndo() 210 | 211 | 212 | if __name__ == '__main__': 213 | ClearConsole() 214 | main() 215 | 216 | 217 | # Licensed under the Apache License, Version 2.0 (the "License"); 218 | # you may not use this file except in compliance with the License. 219 | # You may obtain a copy of the License at 220 | # 221 | # http://www.apache.org/licenses/LICENSE-2.0 222 | # 223 | # Unless required by applicable law or agreed to in writing, software 224 | # distributed under the License is distributed on an "AS IS" BASIS, 225 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 226 | # See the License for the specific language governing permissions and 227 | # limitations under the License. 228 | -------------------------------------------------------------------------------- /docs/api/mesh.rst: -------------------------------------------------------------------------------- 1 | Mesh 2 | ---- 3 | 4 | Functions for working with CINEMA 4D's point and polygon objects. 5 | 6 | 7 | .. function:: TogglePolySelection(obj) 8 | 9 | .. function:: SelectAllPolys(obj) 10 | 11 | .. function:: SelectPolys(li, obj, clearOldSel=True) 12 | 13 | Switch the selection state to 'selected' for a list of polygons. 14 | Expects a list of polygon indices. 15 | 16 | If ``clearOldSel`` is True, clears the old polygon selection. 17 | Otherwise appends to the current selection. Default is True. 18 | 19 | :return: True if the selection state was changed, or False if 20 | obj is not a ``c4d.PolygonObject``. 21 | 22 | .. function:: SelectPoints(li, obj, clearOldSel=True) 23 | 24 | Switch the selection state to 'selected' for a list of points. 25 | Expects a list of point indices. 26 | 27 | If ``clearOldSel`` is True, clears the old polygon selection. 28 | Otherwise appends to the current selection. Default is True. 29 | 30 | :return: True if the selection state was changed, or False if 31 | obj is not a ``c4d.PointObject``. 32 | 33 | .. function:: GetSelectedPoints(obj) 34 | 35 | Returns list of selected point indices. 36 | 37 | To get the actual point(s), using an ``index`` 38 | from the returned list do something like 39 | 40 | .. code:: 41 | 42 | allpoints = op.GetAllPoints() 43 | point = allpoints[index] 44 | 45 | .. function:: GetSelectedPolys(obj) 46 | 47 | Returns list of selected polygons indices. 48 | 49 | To get the actual polygon(s), using an ``index`` 50 | from the returned list do something like 51 | 52 | .. code:: 53 | 54 | allpolys = obj.GetAllPolygons() 55 | poly = allpolys[index] 56 | 57 | .. function:: GetPointsForIndices(li, obj) 58 | 59 | Return a list with the actual points from a list of point indices. 60 | 61 | If ``li`` already is of type ``list`` return the list untouched. 62 | 63 | .. function:: GetPolysForIndices(li, obj) 64 | 65 | Return a list with the actual polygons from a list of polygon indices. 66 | 67 | If ``li`` already is of type ``list`` return the list untouched. 68 | 69 | .. function:: GetIndicesForPoints(lp, obj) 70 | 71 | Return a list of point indices for all points that are equal 72 | to the vectors from lp. 73 | 74 | Warning: can be time consuming for large models, since this 75 | has to check against all points each time for every element 76 | in lp. 77 | 78 | You are better off acquiring the list of indices another way. 79 | Especially if it is just about converting a list of selected 80 | points to their indices. 81 | 82 | Use :py:func:`GetSelectedPoints` in that case. 83 | 84 | If ``lp`` already is of type ``list`` return the list untouched. 85 | 86 | .. function:: GetPolysForPoints(li, obj, strict=True, threshold=3) 87 | 88 | Returns a list of polygon indices for all polygons that have 89 | points with point indices given by ``li`` as their members. 90 | 91 | This is the same as converting between selections by holding 92 | Cmd/Ctrl when pressing the modelling mode buttons in CINEMA 4D. 93 | 94 | :param bool strict: if True, return only those polygons 95 | that are fully enclosed by all the points that make 96 | up that polygon. 97 | 98 | :param int threshold: minimum number of points that must be 99 | shared to for strict mode to be considered as enclosing 100 | a polygon. 3 includes triangles, 4 would only include 101 | quads. 102 | 103 | If ``li`` already is of type ``list`` return the 104 | list untouched. 105 | 106 | .. function:: CalcPolyCentroid(e, obj) 107 | 108 | Calculate the centroid of a polygon by averaging its vertices. 109 | 110 | :param e: can be ``c4d.CPolygon``, ``list`` representing 111 | point indices, or ``list`` representing a list 112 | of points. 113 | 114 | .. function:: CalcPolyNormal(e, obj) 115 | 116 | Calculate the orientation of face normal using Newell's method. 117 | 118 | See :py:func:`CalcVertexNormal` for an example of usage within the calling context. 119 | 120 | :param e: can be ``c4d.CPolygon``, ``list`` representing 121 | point indices, or ``list`` representing a list 122 | of points. 123 | 124 | .. function:: CalcVertexNormal(v, idx, obj) 125 | 126 | Calculate the vertex normal by averaging surrounding face normals. 127 | 128 | Usually called from a construct like the following: 129 | 130 | .. code:: 131 | 132 | # calculate average vertex normal for point selection 133 | vtx_normals = [] 134 | 135 | for i, point in enumerate(obj.GetAllPoints()): 136 | if pointsel.IsSelected(i): 137 | vn = CalcVertexNormal(point, i, obj) 138 | vtx_normals.append(vn) 139 | 140 | N = VAvg(vtx_normals) 141 | 142 | .. function:: CalcAverageVertexNormal(obj) 143 | 144 | Calculate the average normal of a selection of points. 145 | 146 | This gives the same normal as setting the modelling tool 147 | to "Normal" mode for an arbitrary point selection. 148 | 149 | :return: normal, or zero vector if no points selected. 150 | 151 | .. function:: CalcThreePointNormal(a, b, c) 152 | 153 | Calculate the surface normal of a three point plane. 154 | 155 | Doesn't take orientation of neighboring polygons into account. 156 | 157 | .. function:: CalcTriangleArea(p, obj) 158 | 159 | Calculate area of a triangle using ``|(v3 - v1) x (v3 - v2)|/2``. 160 | 161 | .. function:: CalcPolyArea(p, obj, normalized=False) 162 | 163 | Calculate the area of a planar polygon. 164 | 165 | .. function:: CalcBBox(e, selOnly=False, obj=None) 166 | 167 | Construct a :py:class:`BBox` for a ``c4d.PointObject``, a ``c4d.CPolygon``, 168 | or a list of polygon indices. If you have a list of point indices you can 169 | construct a BBox directly using the :py:func:`FromPointList` class method. 170 | 171 | You must supply the object the polygon list belongs to in the latter case. 172 | 173 | Note that if you are interested in the midpoint or radius only, you can 174 | use the built-in ``c4d.BaseObject.GetMp()`` and ``GetRad()`` methods 175 | respectively. 176 | 177 | :param bool selOnly: if True, use selected points 178 | only if e is a ``c4d.PointObject``. Otherwise use 179 | all points of the object. 180 | 181 | .. function:: CalcGravityCenter(obj) 182 | 183 | Calculate the center of gravity for obj. 184 | 185 | .. function:: PolyToList(p) 186 | 187 | Convert a ``c4d.CPolygon`` to a ``list`` of ``c4d.Vectors``, representing the points of the polygon. 188 | 189 | .. function:: PolyToListList(p, obj) 190 | 191 | Convert a ``c4d.CPolygon`` to a ``list`` structure. 192 | 193 | ``list`` represents a list of points comprised of a list of coordinate values. 194 | 195 | .. function:: ListToPoly(li) 196 | 197 | Convert a ``list`` of ``int`` representing indices into an object's point list to a ``c4d.CPolygon``. 198 | 199 | .. function:: ListListToPoly(lli) 200 | 201 | Convert a ``list`` structure to ``c4d.CPolygon``. 202 | 203 | ``list`` represents a list of indices that indentify points of an object. 204 | 205 | -------------------------------------------------------------------------------- /docs/api/utils.rst: -------------------------------------------------------------------------------- 1 | Utils 2 | ----- 3 | 4 | Utility toolbelt for great convenience. 5 | 6 | .. function:: ClearConsole() 7 | 8 | Clears the console across multiple CINEMA 4D versions. 9 | 10 | .. function:: FuzzyCompareStrings(a, b, limit=20) 11 | 12 | Fuzzy string comparison. 13 | 14 | Two strings are deemed equal if they have 15 | the same byte sequence for up to 'limit' chars. 16 | 17 | Limit can be an int or a percentage string 18 | like for example ``60%`` in which case 2 strings 19 | are deemed equal if at least ``60%`` relative to 20 | the longest string match. 21 | 22 | .. function:: EscapeUnicode(s) 23 | 24 | CINEMA 4D R12's CPython integration stores high-order chars (``ord > 126``) 25 | as 4-byte unicode escape sequences with upper case hex letters. 26 | 27 | For example the character ``ä`` (*LATIN SMALL LETTER A WITH DIAERESIS*) 28 | would be stored as the byte sequence ``\u00E4``. This function replaces 29 | high-order chars with a unicode escape sequence suitable for CINEMA 4D R12. 30 | 31 | If you use this function in R12 you probably need to balance each call with a 32 | call to :py:func:`UnescapeUnicode` when the time comes to use, display or 33 | compare your string to a string returned by some internal function of C4D R12. 34 | 35 | For example, if you ask an object named ``Würfel`` (German for cube) 36 | for its name:: 37 | 38 | opname = op.GetName() 39 | 40 | # opname now prints as "W\ürfel" but is actually "W\u00FCrfel" 41 | # if you UnescapeUnicode now you will get a Python byte string 42 | # with proper encoding 43 | 44 | opname_unescaped = UnescapeUnicode(opname) 45 | 46 | # opname_unescaped is now "W\xfcrfel" (latin-1) and you can compare 47 | # it to other Python byte strings with no additional fuzz. If you want 48 | # to change the byte string and then pass it to C4D to set an object's 49 | # new name you would have to escape the byte string to get a string 50 | # that uses an escape sequence similar to the one shown at the beginning 51 | 52 | new_opname_escaped = EscapeUnicode(opname_unescaped) 53 | 54 | op.SetName(new_opname_escaped) 55 | 56 | # Remember this is only relevant for R12. In R13 and R14 both functions 57 | # are practically no-ops. 58 | 59 | In R13 and R14 it returns the string untouched since in those versions 60 | the CPython intergration handles Unicode encoded strings properly. 61 | 62 | .. function:: UnescapeUnicode(s) 63 | 64 | CINEMA 4D R12's CPython integration stores high-order chars (``ord > 126``) 65 | as 4-byte unicode escape sequences with upper case hex letters. 66 | 67 | This function converts unicode escape sequences used by CINEMA 4D when passing 68 | bytes (e.g. ``\u00FC`` -> ``\xfc``) to their corresponding high-order characters. 69 | 70 | It should be used in R12 only and should balance out any calls made to 71 | :py:func:`EscapeUnicode`. 72 | 73 | In R13 and R14 the string is returned untouched since in those versions 74 | the CPython intergration handles Unicode encoded strings properly. 75 | 76 | .. function:: VersionString(versionTuple) 77 | 78 | ``(x,y,z, .. n) -> 'x.y.z...n'`` 79 | 80 | .. function:: PPLLString(ll) 81 | 82 | Returns a pretty-printed string of a ``list`` structure. 83 | 84 | .. function:: System(cmd, args=None) 85 | 86 | Convenience function for firing off commands to 87 | the system console. Used instead of `subprocess.call`_ 88 | so that shell variables will be expanded properly. 89 | 90 | Not the same as `os.system`_ as here it captures 91 | ``stdout`` and ``stderr`` in a tuple for Python 2.5 92 | and lower or a ``namedtuple`` in 2.6 and higher. 93 | So you can use ``result[0]`` in the first case and 94 | ``result.out`` in the second. 95 | 96 | :param str cmd: a console command line string 97 | :param list args: a list of arguments that will be 98 | expanded in cmd starting with ``$0`` 99 | :return: ``tuple`` or ``namedtuple`` 100 | 101 | Decorators 102 | ---------- 103 | 104 | .. function:: benchmark(func=None, prec=3, unit='auto', name_width=0, time_width=8) 105 | 106 | A decorator that prints the time a function takes 107 | to execute per call and cumulative total. 108 | 109 | Accepts the following keyword arguments 110 | 111 | :param str unit: time unit for display. one of ``[auto, us, ms, s, m]``. 112 | :param int prec: radix point precision. 113 | :param int name_width: width of the right-aligned function name field. 114 | :param int time_width: width of the right-aligned time value field. 115 | 116 | For convenience you can also set attributes on the benchmark 117 | function itself with the same name as the keyword arguments 118 | and the value of those will be used instead. This saves you 119 | from having to call the decorator with the same arguments each 120 | time you use it. Just set, for example, ``benchmark.prec = 5`` 121 | after the import and before you use it for the first time. 122 | 123 | Usage example: 124 | 125 | .. code:: 126 | 127 | @benchmark 128 | def factorial(x): 129 | ''' Return factorial of x. ''' 130 | result = 1 131 | for i in range(x): 132 | result = result * (i + 1) 133 | return result 134 | 135 | Output: 136 | 137 | .. code:: 138 | 139 | -> factorial() @ 001: 10.000 us, total: 10.000 us 140 | -> factorial() @ 002: 22.000 us, total: 32.000 us 141 | 142 | Output for ``@benchmark(unit='ms', time_width=6)``: 143 | 144 | .. code:: 145 | 146 | -> factorial() @ 001: 0.009 ms, total: 0.009 ms 147 | -> factorial() @ 002: 0.023 ms, total: 0.032 ms 148 | 149 | .. function:: require(*args, **kwargs) 150 | 151 | Decorator that enforces types for function/method args. 152 | 153 | Two ways to specify which types are required for each arg. 154 | 155 | 1) 2-tuples, where first member specifies arg index or arg name, 156 | second member specifies a type or a tuple of types. 157 | 2) kwargs style, e.g. ``argname=types`` where ``types`` again can 158 | be a type or a tuple of types. 159 | 160 | None is always a valid type, to allow for optional args. 161 | 162 | Usage example: 163 | 164 | .. code:: 165 | 166 | @require(x=int, y=float) 167 | def func(x, y): 168 | return x / y 169 | 170 | .. function:: deprecated(level=1, since=None, info=None) 171 | 172 | Can be used to mark functions as deprecated. 173 | 174 | :param int level: severity level. 175 | 0 = warnings.warn(category=DeprecationWarning) 176 | 1 = warnings.warn_explicit(category=DeprecationWarning) 177 | 2 = raise DeprecationWarning() 178 | :param string since: the version where deprecation was introduced. 179 | :param string info: additional info. normally used to refer to the new 180 | function now favored in place of the deprecated one. 181 | 182 | .. function:: cache(func) 183 | 184 | Classic cache decorator. 185 | 186 | .. function:: memoize(func) 187 | 188 | Classic memoization decorator. 189 | 190 | 191 | .. _subprocess.call: http://docs.python.org/library/subprocess.html?highlight=subprocess.call#subprocess.call 192 | .. _os.system: http://docs.python.org/library/os.html?highlight=os.system#os.system -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/py4dlib.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/py4dlib.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/py4dlib" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/py4dlib" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /source/py4dlib/examples/ShowPolygonNumber.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # ShowPolygonNumber.py 4 | # py4dlib.examples 5 | # 6 | # Created by André Berg on 2013-07-29. 7 | # Copyright 2013 Berg Media. All rights reserved. 8 | # 9 | # andre.bergmedia@googlemail.com 10 | # 11 | # ShowPolygonNumber 12 | # 13 | # Create and attach text splines to polygons. 14 | # 15 | # Summary: 16 | # 17 | # Example script that shows how to get point and polygon selections 18 | # and how to create objects and attach them to polygons in local and 19 | # global coordinate systems. 20 | # 21 | # pylint: disable-msg=F0401 22 | 23 | """ 24 | Name-US:Show Polygon Number 25 | Description-US:Create and attach text splines indicating the polygon number to each selected polygon. 26 | """ 27 | 28 | import os 29 | 30 | __version__ = (0, 2) 31 | __date__ = '2013-07-29' 32 | __updated__ = '2013-08-08' 33 | 34 | 35 | DEBUG = 0 or ('DebugLevel' in os.environ and os.environ['DebugLevel'] > 0) 36 | TESTRUN = 0 or ('TestRunLevel' in os.environ and os.environ['TestRunLevel'] > 0) 37 | 38 | try: 39 | import c4d #@UnresolvedImport 40 | from c4d import documents 41 | except ImportError: 42 | if TESTRUN == 1: 43 | pass 44 | 45 | 46 | if DEBUG: 47 | import py4dlib 48 | reload(py4dlib.mesh) 49 | reload(py4dlib.maths) 50 | 51 | 52 | from py4dlib.maths import BuildMatrix3, IsZeroVector, BBox 53 | from py4dlib.mesh import CalcPolyNormal, CalcPolyCentroid, CalcPolyArea, PolyToListList 54 | from py4dlib.mesh import GetSelectedPoints, GetSelectedPolys 55 | from py4dlib.objects import CreateObject, InsertUnderNull 56 | from py4dlib.utils import ClearConsole, PPLLString 57 | 58 | 59 | # group text spline objects under op else insert at root 60 | GROUP_UNDER = True 61 | TEXT_SIZE = 1 62 | 63 | def main(doc): # IGNORE:W0621 64 | doc.StartUndo() 65 | 66 | sel = doc.GetSelection() 67 | if sel is None: return False 68 | 69 | c4d.StopAllThreads() 70 | 71 | # loop through all objects 72 | for op in sel: 73 | if not isinstance(op, c4d.PolygonObject): 74 | if DEBUG: 75 | print("%s: not a polygon object. Skipping..." % str(op.GetName())) 76 | continue 77 | print("object name: %s" % op.GetName()) 78 | 79 | pointsel = op.GetPointS() 80 | pointselcnt = pointsel.GetCount() 81 | pointcnt = op.GetPointCount() 82 | allpoints = op.GetAllPoints() 83 | 84 | print("number of selected points = %s (%s total)" % (pointselcnt, pointcnt)) 85 | 86 | polysel = op.GetPolygonS() 87 | polyselcnt = polysel.GetCount() 88 | polycnt = op.GetPolygonCount() 89 | allpolys = op.GetAllPolygons() 90 | 91 | 92 | print("number of selected polygons = %s (%s total)" % (polyselcnt, polycnt)) 93 | 94 | pnts = GetSelectedPoints(op) 95 | plys = GetSelectedPolys(op) 96 | 97 | if len(plys) == 0: 98 | # nothing selected? use all polys 99 | plys = list(xrange(0, polycnt)) 100 | 101 | print("selected points = %s" % pnts) 102 | print("selected polys = %s" % plys) 103 | 104 | for pnt in pnts: 105 | print("%d: %s" % (pnt, allpoints[pnt])) 106 | 107 | pmarks = [] 108 | 109 | op_mg = op.GetMg() 110 | op_name = op.GetName() 111 | 112 | pgrp_name = "%s - Polygon #s" % op_name 113 | pgrp = doc.SearchObject(pgrp_name) 114 | if pgrp: 115 | pgrp.Remove() 116 | 117 | for ply in plys: 118 | poly = allpolys[ply] 119 | 120 | a = allpoints[poly.a] 121 | b = allpoints[poly.b] 122 | c = allpoints[poly.c] 123 | d = allpoints[poly.d] 124 | 125 | pids = [a, b, c] 126 | 127 | plen = 3 128 | if c != d: 129 | pids.append(d) 130 | plen = 4 131 | 132 | cv1 = a - b 133 | cv2 = b - c 134 | cv3 = c - d 135 | cv4 = d - a 136 | if plen == 4: 137 | cva = (cv3 - cv1) 138 | cvb = (cv4 - cv2) 139 | else: 140 | cva = cv3 - cv1 141 | cvb = cv3 - cv2 142 | 143 | if plen == 4: 144 | cv = c4d.Vector(cva.x, cvb.y/2, cva.z) 145 | else: 146 | if cvb.y == 0: 147 | cv = cvb 148 | else: 149 | cv = cva 150 | 151 | if IsZeroVector(cv): 152 | if cv == cva: 153 | cv = cvb 154 | else: 155 | cv = cva 156 | 157 | AXIS_ZY = 1 158 | 159 | base = "x" 160 | axis = AXIS_ZY 161 | 162 | if DEBUG: 163 | print("pids = %r" % pids) 164 | print("%d: %s, points as list:" % (ply, poly)) 165 | print("%s" % (PPLLString(PolyToListList(poly, op)))) 166 | 167 | # calculate polygon normals 168 | pnormal = CalcPolyNormal(poly, op) 169 | if DEBUG: print("normal: %s" % (pnormal)) 170 | 171 | # calculate polygon area and bounding box 172 | parea = CalcPolyArea(poly, op) 173 | pbb = BBox.FromPolygon(poly, op) 174 | pbb_slen = pbb.size.GetLength() 175 | parea = (parea / pbb_slen / 2.0) * TEXT_SIZE 176 | 177 | if DEBUG: 178 | print("pbb_slen: %s" % pbb_slen) 179 | print("area: %s" % (parea)) 180 | 181 | # create text spline objects 182 | pname = "%d" % ply 183 | 184 | pmark = CreateObject(c4d.Osplinetext, pname) 185 | pmark[c4d.PRIM_TEXT_TEXT] = pname # Text 186 | pmark[c4d.PRIM_TEXT_HEIGHT] = parea # Font Height 187 | pmark[c4d.PRIM_PLANE] = axis # Orientation 188 | 189 | if GROUP_UNDER: 190 | ppos = CalcPolyCentroid(poly, op) 191 | else: 192 | # put in scene globally and don't group under op 193 | ppos = CalcPolyCentroid(poly, op) * op_mg 194 | 195 | # match position and orientation 196 | pmg = BuildMatrix3(pnormal, cv, off=ppos, base=base) 197 | pmg.v2 = -pmg.v2 198 | 199 | # create translation matrix to center the text letters 200 | tm = c4d.utils.MatrixMove(c4d.Vector(0, -parea/3, 0)) 201 | pmg *= tm 202 | 203 | pmark.SetMg(pmg) 204 | pmarks.append(pmark) 205 | 206 | # group spline text objects under null for each op 207 | pgrp = InsertUnderNull(pmarks, name=pgrp_name) 208 | 209 | if GROUP_UNDER: 210 | pgrp.InsertUnder(op) 211 | 212 | # tell C4D to update internal state 213 | c4d.EventAdd() 214 | doc.EndUndo() 215 | 216 | 217 | if __name__ == '__main__': 218 | ClearConsole() 219 | doc = documents.GetActiveDocument() 220 | main(doc) 221 | 222 | 223 | 224 | # Licensed under the Apache License, Version 2.0 (the "License"); 225 | # you may not use this file except in compliance with the License. 226 | # You may obtain a copy of the License at 227 | # 228 | # http://www.apache.org/licenses/LICENSE-2.0 229 | # 230 | # Unless required by applicable law or agreed to in writing, software 231 | # distributed under the License is distributed on an "AS IS" BASIS, 232 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 233 | # See the License for the specific language governing permissions and 234 | # limitations under the License. 235 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\py4dlib.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\py4dlib.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # py4dlib documentation build configuration file, created by 4 | # sphinx-quickstart on Tue Jul 30 19:53:32 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | sys.path.insert(0, os.path.abspath('../source')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'py4dlib' 44 | copyright = u'2013, André Berg' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = '0.6.5' 52 | # The full version, including alpha/beta/rc tags. 53 | release = '0.6.5' 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = ['_build'] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | # If true, keep warnings as "system message" paragraphs in the built documents. 90 | #keep_warnings = False 91 | 92 | 93 | # -- Options for HTML output --------------------------------------------------- 94 | 95 | # The theme to use for HTML and HTML Help pages. See the documentation for 96 | # a list of builtin themes. 97 | html_theme = 'pydocs3theme' 98 | 99 | # Theme options are theme-specific and customize the look and feel of a theme 100 | # further. For a list of options available for each theme, see the 101 | # documentation. 102 | #html_theme_options = {} 103 | 104 | # Add any paths that contain custom themes here, relative to this directory. 105 | html_theme_path = ['.'] 106 | 107 | # The name for this set of Sphinx documents. If None, it defaults to 108 | # " v documentation". 109 | #html_title = None 110 | 111 | # A shorter title for the navigation bar. Default is the same as html_title. 112 | #html_short_title = None 113 | 114 | # The name of an image file (relative to this directory) to place at the top 115 | # of the sidebar. 116 | #html_logo = None 117 | 118 | # The name of an image file (within the static path) to use as favicon of the 119 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 120 | # pixels large. 121 | #html_favicon = None 122 | 123 | # Add any paths that contain custom static files (such as style sheets) here, 124 | # relative to this directory. They are copied after the builtin static files, 125 | # so a file named "default.css" will overwrite the builtin "default.css". 126 | html_static_path = ['_static'] 127 | 128 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 129 | # using the given strftime format. 130 | #html_last_updated_fmt = '%b %d, %Y' 131 | 132 | # If true, SmartyPants will be used to convert quotes and dashes to 133 | # typographically correct entities. 134 | #html_use_smartypants = True 135 | 136 | # Custom sidebar templates, maps document names to template names. 137 | #html_sidebars = {} 138 | 139 | # Additional templates that should be rendered to pages, maps page names to 140 | # template names. 141 | #html_additional_pages = {} 142 | 143 | # If false, no module index is generated. 144 | #html_domain_indices = True 145 | 146 | # If false, no index is generated. 147 | #html_use_index = True 148 | 149 | # If true, the index is split into individual pages for each letter. 150 | #html_split_index = False 151 | 152 | # If true, links to the reST sources are added to the pages. 153 | #html_show_sourcelink = True 154 | 155 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 156 | #html_show_sphinx = True 157 | 158 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 159 | #html_show_copyright = True 160 | 161 | # If true, an OpenSearch description file will be output, and all pages will 162 | # contain a tag referring to it. The value of this option must be the 163 | # base URL from which the finished HTML is served. 164 | #html_use_opensearch = '' 165 | 166 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 167 | #html_file_suffix = None 168 | 169 | # Output file base name for HTML help builder. 170 | htmlhelp_basename = 'py4dlibdoc' 171 | 172 | 173 | # -- Options for LaTeX output -------------------------------------------------- 174 | 175 | latex_elements = { 176 | # The paper size ('letterpaper' or 'a4paper'). 177 | #'papersize': 'letterpaper', 178 | 179 | # The font size ('10pt', '11pt' or '12pt'). 180 | #'pointsize': '10pt', 181 | 182 | # Additional stuff for the LaTeX preamble. 183 | #'preamble': '', 184 | } 185 | 186 | # Grouping the document tree into LaTeX files. List of tuples 187 | # (source start file, target name, title, author, documentclass [howto/manual]). 188 | latex_documents = [ 189 | ('index', 'py4dlib.tex', u'py4dlib Documentation', 190 | u'André Berg', 'manual'), 191 | ] 192 | 193 | # The name of an image file (relative to this directory) to place at the top of 194 | # the title page. 195 | #latex_logo = None 196 | 197 | # For "manual" documents, if this is true, then toplevel headings are parts, 198 | # not chapters. 199 | #latex_use_parts = False 200 | 201 | # If true, show page references after internal links. 202 | #latex_show_pagerefs = False 203 | 204 | # If true, show URL addresses after external links. 205 | #latex_show_urls = False 206 | 207 | # Documents to append as an appendix to all manuals. 208 | #latex_appendices = [] 209 | 210 | # If false, no module index is generated. 211 | #latex_domain_indices = True 212 | 213 | 214 | # -- Options for manual page output -------------------------------------------- 215 | 216 | # One entry per manual page. List of tuples 217 | # (source start file, name, description, authors, manual section). 218 | man_pages = [ 219 | ('index', 'py4dlib', u'py4dlib Documentation', 220 | [u'André Berg'], 1) 221 | ] 222 | 223 | # If true, show URL addresses after external links. 224 | #man_show_urls = False 225 | 226 | 227 | # -- Options for Texinfo output ------------------------------------------------ 228 | 229 | # Grouping the document tree into Texinfo files. List of tuples 230 | # (source start file, target name, title, author, 231 | # dir menu entry, description, category) 232 | texinfo_documents = [ 233 | ('index', 'py4dlib', u'py4dlib Documentation', 234 | u'André Berg', 'py4dlib', 'One line description of project.', 235 | 'Miscellaneous'), 236 | ] 237 | 238 | # Documents to append as an appendix to all manuals. 239 | #texinfo_appendices = [] 240 | 241 | # If false, no module index is generated. 242 | #texinfo_domain_indices = True 243 | 244 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 245 | #texinfo_show_urls = 'footnote' 246 | 247 | # If true, do not generate a @detailmenu in the "Top" node's menu. 248 | #texinfo_no_detailmenu = False 249 | -------------------------------------------------------------------------------- /docs/pydocs3theme/static/basic.css_t: -------------------------------------------------------------------------------- 1 | /** 2 | * Sphinx stylesheet -- basic theme 3 | * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 4 | */ 5 | 6 | /* -- main layout ----------------------------------------------------------- */ 7 | 8 | div.clearer { 9 | clear: both; 10 | } 11 | 12 | /* -- relbar ---------------------------------------------------------------- */ 13 | 14 | div.related { 15 | width: 100%; 16 | font-size: 90%; 17 | } 18 | 19 | div.related h3 { 20 | display: none; 21 | } 22 | 23 | div.related ul { 24 | margin: 0; 25 | padding: 0 0 0 10px; 26 | list-style: none; 27 | } 28 | 29 | div.related li { 30 | display: inline; 31 | } 32 | 33 | div.related li.right { 34 | float: right; 35 | margin-right: 5px; 36 | } 37 | 38 | /* -- sidebar --------------------------------------------------------------- */ 39 | 40 | div.sphinxsidebarwrapper { 41 | padding: 10px 5px 0 10px; 42 | word-wrap: break-word; 43 | } 44 | 45 | div.sphinxsidebar { 46 | float: left; 47 | width: 230px; 48 | margin-left: -100%; 49 | font-size: 90%; 50 | } 51 | 52 | div.sphinxsidebar ul { 53 | list-style: none; 54 | } 55 | 56 | div.sphinxsidebar ul ul, 57 | div.sphinxsidebar ul.want-points { 58 | margin-left: 20px; 59 | list-style: square; 60 | } 61 | 62 | div.sphinxsidebar ul ul { 63 | margin-top: 0; 64 | margin-bottom: 0; 65 | } 66 | 67 | div.sphinxsidebar form { 68 | margin-top: 10px; 69 | } 70 | 71 | div.sphinxsidebar input { 72 | border: 1px solid #98dbcc; 73 | font-family: sans-serif; 74 | font-size: 1em; 75 | } 76 | 77 | img { 78 | border: 0; 79 | } 80 | 81 | /* -- search page ----------------------------------------------------------- */ 82 | 83 | ul.search { 84 | margin: 10px 0 0 20px; 85 | padding: 0; 86 | } 87 | 88 | ul.search li { 89 | padding: 5px 0 5px 20px; 90 | background-image: url(file.png); 91 | background-repeat: no-repeat; 92 | background-position: 0 7px; 93 | } 94 | 95 | ul.search li a { 96 | font-weight: bold; 97 | } 98 | 99 | ul.search li div.context { 100 | color: #888; 101 | margin: 2px 0 0 30px; 102 | text-align: left; 103 | } 104 | 105 | ul.keywordmatches li.goodmatch a { 106 | font-weight: bold; 107 | } 108 | 109 | /* -- index page ------------------------------------------------------------ */ 110 | 111 | table.contentstable { 112 | width: 90%; 113 | } 114 | 115 | table.contentstable p.biglink { 116 | line-height: 150%; 117 | } 118 | 119 | a.biglink { 120 | font-size: 1.3em; 121 | } 122 | 123 | span.linkdescr { 124 | font-style: italic; 125 | padding-top: 5px; 126 | font-size: 90%; 127 | } 128 | 129 | /* -- general index --------------------------------------------------------- */ 130 | 131 | table.indextable td { 132 | text-align: left; 133 | vertical-align: top; 134 | } 135 | 136 | table.indextable dl, table.indextable dd { 137 | margin-top: 0; 138 | margin-bottom: 0; 139 | } 140 | 141 | table.indextable tr.pcap { 142 | height: 10px; 143 | } 144 | 145 | table.indextable tr.cap { 146 | margin-top: 10px; 147 | background-color: #f2f2f2; 148 | } 149 | 150 | img.toggler { 151 | margin-right: 3px; 152 | margin-top: 3px; 153 | cursor: pointer; 154 | } 155 | 156 | /* -- general body styles --------------------------------------------------- */ 157 | 158 | a.headerlink { 159 | visibility: hidden; 160 | } 161 | 162 | h1:hover > a.headerlink, 163 | h2:hover > a.headerlink, 164 | h3:hover > a.headerlink, 165 | h4:hover > a.headerlink, 166 | h5:hover > a.headerlink, 167 | h6:hover > a.headerlink, 168 | dt:hover > a.headerlink { 169 | visibility: visible; 170 | } 171 | 172 | div.body p.caption { 173 | text-align: inherit; 174 | } 175 | 176 | div.body td { 177 | text-align: left; 178 | } 179 | 180 | .field-list ul { 181 | padding-left: 1em; 182 | } 183 | 184 | .first { 185 | margin-top: 0 !important; 186 | } 187 | 188 | p.rubric { 189 | margin-top: 30px; 190 | font-weight: bold; 191 | } 192 | 193 | /* -- sidebars -------------------------------------------------------------- */ 194 | 195 | div.sidebar { 196 | margin: 0 0 0.5em 1em; 197 | border: 1px solid #ddb; 198 | padding: 7px 7px 0 7px; 199 | background-color: #ffe; 200 | width: 40%; 201 | float: right; 202 | } 203 | 204 | p.sidebar-title { 205 | font-weight: bold; 206 | } 207 | 208 | /* -- topics ---------------------------------------------------------------- */ 209 | 210 | div.topic { 211 | border: 1px solid #ccc; 212 | padding: 7px 7px 0 7px; 213 | margin: 10px 0 10px 0; 214 | } 215 | 216 | p.topic-title { 217 | font-size: 1.1em; 218 | font-weight: bold; 219 | margin-top: 10px; 220 | } 221 | 222 | /* -- admonitions ----------------------------------------------------------- */ 223 | 224 | div.admonition { 225 | margin-top: 10px; 226 | margin-bottom: 10px; 227 | padding: 7px; 228 | } 229 | 230 | div.admonition dt { 231 | font-weight: bold; 232 | } 233 | 234 | div.admonition dl { 235 | margin-bottom: 0; 236 | } 237 | 238 | p.admonition-title { 239 | margin: 0px 10px 5px 0px; 240 | font-weight: bold; 241 | } 242 | 243 | div.body p.centered { 244 | text-align: center; 245 | margin-top: 25px; 246 | } 247 | 248 | /* -- tables ---------------------------------------------------------------- */ 249 | 250 | table.docutils { 251 | border: 0 solid #dce; 252 | border-collapse: collapse; 253 | } 254 | 255 | table.docutils td, table.docutils th { 256 | padding: 2px 5px 2px 5px; 257 | border-left: 0; 258 | background-color: #eef; 259 | } 260 | 261 | table.docutils td p.last, table.docutils th p.last { 262 | margin-bottom: 0; 263 | } 264 | 265 | table.field-list td, table.field-list th { 266 | border: 0 !important; 267 | } 268 | 269 | table.footnote td, table.footnote th { 270 | border: 0 !important; 271 | } 272 | 273 | table.docutils th { 274 | border-top: 1px solid #cac; 275 | background-color: #ede; 276 | } 277 | 278 | th { 279 | text-align: left; 280 | padding-right: 5px; 281 | } 282 | 283 | th.head { 284 | text-align: center; 285 | } 286 | 287 | /* -- other body styles ----------------------------------------------------- */ 288 | 289 | dl { 290 | margin-bottom: 15px; 291 | } 292 | 293 | dd p { 294 | margin-top: 0px; 295 | } 296 | 297 | dd ul, dd table { 298 | margin-bottom: 10px; 299 | } 300 | 301 | dd { 302 | margin-top: 3px; 303 | margin-bottom: 10px; 304 | margin-left: 30px; 305 | } 306 | 307 | dt:target, .highlight { 308 | background-color: #fbe54e; 309 | } 310 | 311 | dl.glossary dt { 312 | font-weight: bold; 313 | font-size: 1.1em; 314 | } 315 | 316 | .field-list ul { 317 | margin: 0; 318 | padding-left: 1em; 319 | } 320 | 321 | .field-list p { 322 | margin: 0; 323 | } 324 | 325 | .refcount { 326 | color: #060; 327 | } 328 | 329 | .optional { 330 | font-size: 1.3em; 331 | } 332 | 333 | .versionmodified { 334 | font-style: italic; 335 | } 336 | 337 | p.deprecated, p.deprecated-removed { 338 | background-color: #ffe4e4; 339 | border: 1px solid #f66; 340 | padding: 7px 341 | } 342 | 343 | .system-message { 344 | background-color: #fda; 345 | padding: 5px; 346 | border: 3px solid red; 347 | } 348 | 349 | .footnote:target { 350 | background-color: #ffa; 351 | } 352 | 353 | .impl-detail { 354 | margin-top: 10px; 355 | margin-bottom: 10px; 356 | padding: 7px; 357 | border: 1px solid #ccc; 358 | } 359 | 360 | .impl-detail .compound-first { 361 | margin-top: 0; 362 | } 363 | 364 | .impl-detail .compound-last { 365 | margin-bottom: 0; 366 | } 367 | 368 | /* -- code displays --------------------------------------------------------- */ 369 | 370 | pre { 371 | overflow: auto; 372 | overflow-y: hidden; 373 | } 374 | 375 | td.linenos pre { 376 | padding: 5px 0px; 377 | border: 0; 378 | background-color: transparent; 379 | color: #aaa; 380 | } 381 | 382 | table.highlighttable { 383 | margin-left: 0.5em; 384 | } 385 | 386 | table.highlighttable td { 387 | padding: 0 0.5em 0 0.5em; 388 | } 389 | 390 | tt.descname { 391 | background-color: transparent; 392 | font-weight: bold; 393 | font-size: 1.2em; 394 | } 395 | 396 | tt.descclassname { 397 | background-color: transparent; 398 | } 399 | 400 | tt.xref, a tt { 401 | background-color: transparent; 402 | font-weight: bold; 403 | } 404 | 405 | h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt { 406 | background-color: transparent; 407 | } 408 | 409 | /* -- math display ---------------------------------------------------------- */ 410 | 411 | img.math { 412 | vertical-align: middle; 413 | } 414 | 415 | div.body div.math p { 416 | text-align: center; 417 | } 418 | 419 | span.eqno { 420 | float: right; 421 | } 422 | 423 | /* -- printout stylesheet --------------------------------------------------- */ 424 | 425 | @media print{ 426 | div.document, 427 | div.documentwrapper, 428 | div.bodywrapper { 429 | margin: 0 !important; 430 | width: 100%; 431 | } 432 | 433 | div.sphinxsidebar, 434 | div.related, 435 | div.footer, 436 | #top-link { 437 | display: none; 438 | } 439 | } 440 | -------------------------------------------------------------------------------- /docs/api/maths.rst: -------------------------------------------------------------------------------- 1 | Maths 2 | ----- 3 | 4 | 5 | .. class:: BBox 6 | 7 | Calculate various bounding box metrics from a list of points, 8 | such as min, max, midpoint, radius and size. 9 | 10 | .. classmethod:: FromObject(cls, obj, selOnly=False) 11 | 12 | Returns a new BBox object with all points from the passed object. 13 | 14 | :param bool selOnly: use selected points only instead of all points. 15 | 16 | :raise ValueError: if the object has no points. 17 | 18 | .. classmethod:: FromPointList(cls, lst) 19 | 20 | Returns a new BBox object with all points from a list added. 21 | 22 | Elements of lst must be of type ``c4d.Vector``. 23 | 24 | :raise ValueError: if the list is empty. 25 | 26 | .. classmethod:: FromPolygon(cls, poly, obj) 27 | 28 | Returns a new BBox object with all points from the passed polygon. 29 | 30 | .. function:: AddPoint(p) 31 | 32 | Add metrics from point p. 33 | 34 | .. function:: AddPoints(lst) 35 | 36 | Add metrics from a list of points. 37 | 38 | .. function:: GetMax() 39 | 40 | Return max bounds vector. 41 | 42 | .. function:: GetMin() 43 | 44 | Return min bounds vector. 45 | 46 | .. function:: GetMp() 47 | 48 | Return midpoint vector. 49 | 50 | .. function:: GetRad() 51 | 52 | Return radius vector. 53 | 54 | .. function:: GetSize() 55 | 56 | Return size vector. 57 | 58 | .. class:: Plane(pos, n) 59 | 60 | Represents a plane defined by positional offset and normal direction. 61 | 62 | .. function:: SetN(self, newN) 63 | 64 | Sets the normal of the plane to ``newN``. 65 | 66 | .. function:: SetPos(self, newPos) 67 | 68 | Sets the positional offset of the plane to ``newPos``. 69 | 70 | .. function:: SideAsString(self, d) 71 | 72 | Given distance ``d`` return a string indicating the residence 73 | of the point that correlates to the given distance. 74 | 75 | Used together with :py:func:`PointResidence`:: 76 | 77 | SideAsString(PointResidence(self, p)) 78 | 79 | :return: either ``front``, ``back`` or ``onplane`` 80 | 81 | .. function:: PointResidence(self, p) 82 | 83 | Define the resident direction of a point with respect 84 | to the plane. 85 | 86 | The point can be either in front of the plane (+1), on the 87 | plane (0) or at the back of the plane (-1). 88 | 89 | 90 | .. function:: PointDistance(self, p, signed=True) 91 | 92 | Calculate distance from a point p to the plane. 93 | 94 | :param bool signed: set to True if you want the signed distance. 95 | 96 | A signed distance can be useful to determine if the point is located 97 | in the half space from the backside of the plane or in the half space 98 | on the front. 99 | 100 | .. function:: LineIntersection(self, p, d=None) 101 | 102 | Calculate intersection point with a line starting at position p 103 | and pointing in the direction d. 104 | 105 | :param c4d.Vector d: direction of the line. If None, the normal 106 | of the plane will be used instead. 107 | 108 | :return: ``c4d.Vector`` representing the intersection point, or 109 | None if an intersection isn't possible (parallel directions). 110 | 111 | 112 | .. function:: FloatEqual(a, b, places=8) 113 | 114 | Same as ``c4d.utils.FloatTolerantCompare`` just a shorter function name. 115 | 116 | .. function:: MAbs(m) 117 | 118 | ``abs()`` each component vector of matrix m. 119 | 120 | .. function:: VDeg(v, isHPB=False) 121 | 122 | Convert each component of vector v to degrees. 123 | 124 | .. function:: VRad(v, isHPB=False) 125 | 126 | Convert each component of vector v to radians. 127 | 128 | .. function:: VAvg(lv) 129 | 130 | Calculate the average of a list of vectors. 131 | 132 | .. function:: VAbsMin(v) 133 | 134 | Return min component of a vector using ``abs(x) < abs(y)`` comparisons. 135 | 136 | .. function:: VAbsMax(v) 137 | 138 | Return max component of a vector using ``abs(x) > abs(y)`` comparisons. 139 | 140 | .. function:: VBoundaryLerp(lv, t=0.5) 141 | 142 | Interpolate linearily between a list of vectors, such that 143 | the resulting vector points to the weighted midpoint in the 144 | vector space defined by the boundaries max X to min X and 145 | max Y to min Y. 146 | 147 | :param float t: the weighting coefficient. 148 | 149 | :return: None if len(lst) is 0 or if the angle between 150 | the two max/min vectors is greater than 180 degrees. 151 | 152 | .. function:: VLerp(startv, endv, t=0.5) 153 | 154 | Linear interpolation between 2 vectors. 155 | 156 | Same as ``c4d.utils.VectorMix``. 157 | 158 | .. function:: VNLerp(startv, endv, t=0.5) 159 | 160 | Normalized linear interpolation between 2 vectors. 161 | 162 | .. function:: VSLerp(startv, endv, t=0.5) 163 | 164 | Spherical linear interpolation between 2 vectors. 165 | 166 | .. function:: BuildMatrix(v, off=None, order="zyx") 167 | 168 | Builds a new orthonormal basis from a direction and (optionally) an offset vector using John F. Hughes and Thomas Möller's method. 169 | 170 | If ``off`` is None, off will default to a zero vector. 171 | 172 | .. function:: BuildMatrix2(v, off=None, base="z") 173 | 174 | Builds a new orthonormal basis from a direction and (optionally) an offset vector using base aligned cross products. 175 | 176 | If ``off`` is None, off will default to a zero vector. 177 | 178 | :param str base: the base component 'v' represents. Must be one of ``x, y, z, -x, -y, -z`` 179 | 180 | 181 | .. function:: BuildMatrix3(v, v2, off=None, base="z") 182 | 183 | Builds a new orthonormal basis from 2 direction 184 | and (optionally) an offset vector using cross products. 185 | 186 | :param str base: the base component 'v' represents. 187 | 188 | .. function:: GetMulP(m, v) 189 | 190 | Multiply a matrix with a vector representing a point. 191 | 192 | Same as ``c4d.Matrix.Mul(v)``. 193 | 194 | .. function:: GetMulV(m, v) 195 | 196 | Multiply a matrix with a vector representing a direction. 197 | 198 | Same as ``c4d.Matrix.MulV(v)`` 199 | 200 | .. function:: Det(m) 201 | 202 | Determinant of an ``n x n`` matrix. 203 | 204 | m can be of type ``c4d.Matrix`` when ``n = 3`` 205 | or ``list`` when ``n = 3`` or ``n = 4`` . 206 | 207 | .. function:: Transpose(e) 208 | 209 | Transpose matrix e in row-major format to column-major. 210 | 211 | ``e`` can be of type ``list`` structure or ``c4d.Matrix``. 212 | 213 | .. function:: ListToMatrix(lv) 214 | 215 | Convert a list of 3 or 4 ``c4d.Vector`` to ``c4d.Matrix``. 216 | 217 | .. function:: ListListToMatrix(lli) 218 | 219 | Convert a ``list`` structure, representing a list of list 220 | of coordinate values to a ``c4d.Matrix``. 221 | 222 | See :py:func:`MatrixToListList` to find out which list corresponds 223 | to which matrix component. 224 | 225 | .. function:: MatrixToListList(m, inclOff=False) 226 | 227 | Convert a ``c4d.Matrix`` to a ``list`` structure. 228 | 229 | The structure layout is generally in row-major format, 230 | and the ordering the same as the order required for 231 | constructing a ``c4d.Matrix`` by hand: 232 | 233 | .. code:: 234 | 235 | [[off.x, off.y, off.z], 236 | [v1.x, v1.y, v1.z], 237 | [v2.x, v2.y, v2.z], 238 | [v3.x, v3.y, v3.z]] 239 | 240 | .. function:: UnitNormal(a, b, c) 241 | 242 | Calculate unit normal of a planar surface. 243 | 244 | :raise ValueError: if magnitude <= 0.0 245 | 246 | .. function:: IsPointInTriangle(p, a, b, c) 247 | 248 | Returns True if the point p is inside the triangle given by points a, b, and c. 249 | 250 | .. function:: IsColinear(lv) 251 | 252 | Given a list of vectors check if they all share the same coordinates 253 | in at least 2 dimensions. 254 | 255 | :return: True if all the vectors in the list are co-linear. 256 | 257 | .. function:: IsZeroVector(v) 258 | 259 | Uses float tolerant component comparison to check if v is a zero vector. 260 | 261 | .. function:: LineLineDistance(p1a, p1b, p2a, p2b) 262 | 263 | Computes the smallest distance between two 3D lines. 264 | 265 | :return: ``tuple`` of two ``c4d.Vectors`` which are the points on each of the 266 | two input lines that, when connected, form a segment which represents the 267 | shortest distance between the two lines. 268 | 269 | .. function:: WrapPi(theta) 270 | 271 | Wraps an angle theta in range ``-pi..pi`` by adding the correct multiple of 2 pi. 272 | 273 | .. function:: SafeAcos(x) 274 | 275 | Same as ``math.acos(x)`` but if x is out of range, it is *clamped* to the 276 | nearest valid value. The value returned is in range ``0..pi``, the same as 277 | the standard `math.acos`_ function. 278 | 279 | 280 | .. _math.acos: http://docs.python.org/2/library/math.html?highlight=math.acos#math.acos 281 | -------------------------------------------------------------------------------- /docs/api/objects.rst: -------------------------------------------------------------------------------- 1 | Objects 2 | ------- 3 | 4 | Functions for working with CINEMA 4D's objects. 5 | 6 | .. class:: ObjectIterator(start_obj, stop_obj=None, children_only=True, startlvl=-1) 7 | 8 | Iterator over specific objects in the object manager tree. 9 | 10 | Using a depth first traversal scheme, return a tuple in the form 11 | ``(op, lvl)``, where op is a ``c4d.BaseObject`` representing the current 12 | object and lvl is an integer indicating the current depth level. 13 | 14 | :param c4d.BaseObject start_obj: the object whose hierarchy should be iterated over 15 | :param c4d.BaseObject stop_obj: an object or a list of objects at which traversal 16 | should stop (optional) 17 | :param bool children_only: if True, iterate through the sub-hierarchy under 18 | startobj and stop as soon as startobj's parent or 19 | stopobj (if given) is reached. This excludes startobj 20 | from the iteration. 21 | :param int startlvl: base indentation level 22 | 23 | .. class:: ObjectEntry(op, lvl=-1, parents=None) 24 | 25 | Wraps ``c4d.BaseObject`` and makes them hashable, 26 | so they can be used as keys in dictionaries. 27 | 28 | :param c4d.BaseObject op: the object to wrap. 29 | :param int lvl: the depth level within the hierarchy. 30 | :param list parents: a list of parent objects 31 | 32 | .. class:: ObjectHierarchy(root_obj=None, filter_type=None, children_only=False) 33 | 34 | Represents a hierarchical group structure in the object manager. 35 | 36 | Can be used to create a Pythonic snapshot of the current scene 37 | so as to provide easy access to specifc sets of objects. 38 | 39 | Starting with root object stores a list of ``c4d.BaseObjects`` 40 | for each depth level in a dictionary. Each list is indexed by a 41 | concatenation of its parent names. The concat character is a 42 | forward slash, which forms a Unix like filepath as seen with 43 | the object manager's address bar widget. 44 | 45 | Additionally, a small subset of X-Path like functionality is 46 | provided with the ``Get()`` function, namely the subset that 47 | coincides with syntax for wildcard and regular epxression 48 | expansion. This makes it easy to select a subset of objects, 49 | based on parent-name relationships. 50 | 51 | :param c4d.Otype filter_type: only recognize objects of this c4d type 52 | :param bool children_only: see :py:class:`ObjectIterator` 53 | 54 | .. function:: PPrint(stopobj=None, filtertype=None, tabsize=4) 55 | 56 | Print an indented, tree-like representation of an object manager hierarchy. 57 | 58 | .. function:: Get(path, strict=True) 59 | 60 | Get a list of ``c4d.BaseObject`` for the key path given by 'path'. 61 | 62 | Key path can contain wildcards (``*`` or ``?``) or regular expression 63 | syntax. Prepend a ``!`` to ``path`` if you want to forego wildcard expansion 64 | and thus ensure it is used as a verbatim regular expression pattern instead. 65 | 66 | Note that if ``strict`` is True, ``path`` must match the whole key it is 67 | tested against. Otherwise it is sufficient if the path is contained by 68 | the key. 69 | 70 | Returns a list of all objects for which ``path``, expanded, matches a 71 | concatenated parent path. 72 | 73 | Returns an empty list if no objects could be located for ``path``. 74 | 75 | .. function:: Select(obj) 76 | 77 | .. function:: SelectAdd(obj) 78 | 79 | Same as :py:func:`Select` but uses a slightly different mechanism. 80 | 81 | See also ``BaseDocument.SetSelection(sel, mode)``. 82 | 83 | .. function:: SelectGroupMembers(grp) 84 | 85 | .. function:: SelectObjects(objs) 86 | 87 | .. function:: DeselectAll(inObjMngr=False) 88 | 89 | Not the same as ``BaseSelect.DeselectAll()``. 90 | 91 | :param bool inObjMngr: if True, run the deselect command for the 92 | Object Manager, else the general one for the editor viewport. 93 | 94 | .. function:: GroupObjects(objs, name="Group") 95 | 96 | ``CallCommand`` based grouping of objects from a list. 97 | Generally unreliable, because selection state matters. 98 | 99 | Use :py:func:`InsertUnderNull` for better effect. 100 | 101 | .. function:: GroupSelected(name="Group") 102 | 103 | ``CallCommand`` based grouping of selected objects. 104 | Generally unreliable, because selection state matters. 105 | 106 | Use :py:func:`InsertUnderNull` for better effect. 107 | 108 | .. function:: RecurseBranch(obj) 109 | 110 | .. function:: GetNextObject(obj, stop_objs=None) 111 | 112 | Return the next object in the hierarchy using a depth-first traversal scheme. 113 | 114 | If stop_objs is a ``c4d.BaseObject`` or a list of ``c4d.BaseObjects`` and the next 115 | operation would encounter this object (or the first object in the list) None 116 | will be returned. This is so that this function can be used in a while loop. 117 | 118 | .. function:: GetActiveObjects(doc) 119 | 120 | Same as ``BaseDocument.GetSelection()``, while 121 | GetSelection also selects tags and materials. 122 | 123 | .. function:: FindObject(name, start=None, matchfunc=None, *args, **kwargs) 124 | 125 | Find object with name 'name'. 126 | 127 | :param c4d.BaseObject start: object from where the search should begin. 128 | Can also be a ``str`` representing the name of a ``c4d.BaseObject``. 129 | :param function matchfunc: can be used to customize the matching logic 130 | by providing the name of a custom function. This function 131 | will be passed a potential candidate object plus any 132 | remaining args. It should return True or False. 133 | 134 | .. function:: FindObjects(name=None, uip=None) 135 | 136 | Find all objects in the scene, either with the name ``name`` 137 | and/or the unique IP ``uip``. 138 | 139 | :return: list with matched objects or empty list if no match. 140 | 141 | .. function:: CreateObject(typ, name, undo=True) 142 | 143 | Create a object of type 'typ', with name 'name'. 144 | 145 | This calls ``c4d.StopAllThreads()`` internally. 146 | 147 | .. function:: CreateReplaceObject(typ, name) 148 | 149 | Create object with name 'name' removing and replacing any object with the same name. 150 | 151 | This calls :py:func:`CreateObject` internally. 152 | 153 | .. function:: UniqueSequentialName(name_base, template=u'%(name)s.%(num)s') 154 | 155 | Return a new sequential name based on a naming template and a 156 | base name such that the name uniquely identifies an object in 157 | the scene. 158 | 159 | By default, mimicks the names generated by CINEMA 4D when 160 | multiple objects of the same type are created in quick succession. 161 | 162 | For example if the scene had the following objects:: 163 | 164 | Cube 165 | Cube.1 166 | Cube.12 167 | 168 | Using the default template, the function would return ``Cube.13`` 169 | as a new name. 170 | 171 | .. function:: InsertUnderNull(objs, grp=None, name="Group", copy=False) 172 | 173 | Inserts objects under a group (null) object, optionally creating the group. 174 | 175 | Note: currently does not reset obj's coordinate frame 176 | to that of the new parent. 177 | 178 | :param c4d.BaseObject objs: can be a single object or a list of objects 179 | :param c4d.BaseObject grp: the group to place the objects under. If None 180 | a new null object will be created. 181 | :param str name: name for the new group 182 | :param bool copy: copy the objects if True 183 | 184 | .. function:: GetGlobalPosition(obj) 185 | 186 | .. function:: GetGlobalRotation(obj) 187 | 188 | .. function:: GetGlobalScale(obj) 189 | 190 | .. function:: SetGlobalPosition(obj, pos) 191 | 192 | .. function:: SetGlobalRotation(obj, rot) 193 | 194 | Please remember, like most 3D engines 195 | CINEMA 4D handles rotation in radians. 196 | 197 | Example for ``H=10, P=20, B=30``: 198 | 199 | .. code:: 200 | 201 | import c4d 202 | from c4d import utils 203 | # ... 204 | hpb = c4d.Vector(utils.Rad(10), utils.Rad(20), utils.Rad(30)) 205 | SetGlobalRotation(obj, hpb) # object's rotation is 10, 20, 30 206 | 207 | 208 | .. function:: SetGlobalScale(obj, scale) 209 | 210 | .. function:: SetAxisRotation(obj, rot, local=False) 211 | 212 | Set the rotation of the object axis (i.e. keeping points in place). 213 | 214 | :param obj: object 215 | :param rot: vector 216 | 217 | Courtesy of Scott Ayers (`source `_) 218 | 219 | 220 | .. function:: CenterObjectAxis(obj, center="midpoint") 221 | 222 | Center the object's axis. 223 | 224 | This is equivalent to moving the object to the new center 225 | and then moving all the points back to the old spot. 226 | 227 | :param str center: can be ``midpoint`` to use the center 228 | of the object's bounding box, or ``gravity`` to use the 229 | object's center of gravity. The difference is that in the 230 | latter case single points at extreme distances from the 231 | object's core aren't given as much weight. 232 | 233 | 234 | .. function:: ObjectAxisFromVector(v) 235 | 236 | Same as ``c4d.utils.HPBToMatrix(c4d.utils.VectorToHPB(v))``. 237 | 238 | .. function:: MakeEditable(obj, clone=False) 239 | 240 | Run the Make Editible command on an object or a list of objects. 241 | 242 | :param bool clone: if True, return editable clones of the 243 | input. 244 | 245 | :return: editable object or list of editable objects 246 | Returns False if the document of the input objects 247 | can't be found. -------------------------------------------------------------------------------- /source/py4dlib/plugins.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # plugins.py 4 | # py4dlib 5 | # 6 | # Created by André Berg on 2012-09-26. 7 | # Copyright 2012 Berg Media. All rights reserved. 8 | # 9 | # andre.bergmedia@googlemail.com 10 | # 11 | # pylint: disable-msg=F0401 12 | 13 | '''py4dlib.plugins -- functions for helping you write py4d plugins.''' 14 | 15 | import os 16 | import ConfigParser 17 | 18 | __version__ = (0, 5) 19 | __date__ = '2012-09-26' 20 | __updated__ = '2013-08-13' 21 | 22 | DEBUG = 0 or ('DebugLevel' in os.environ and os.environ['DebugLevel'] > 0) 23 | TESTRUN = 0 or ('TestRunLevel' in os.environ and os.environ['TestRunLevel'] > 0) 24 | 25 | 26 | class UserDefaults(object): 27 | """ 28 | Support for reading and writing settings files 29 | in .ini format. 30 | 31 | This can be used to provide state persistence 32 | for UI elements of a plugin. 33 | 34 | Examples: 35 | 36 | Initialize a new config object, modify and 37 | then save it: 38 | 39 | >>> filepath = os.path.join(os.path.dirname(os.path.abspath(__file__)), "res", "settings.ini") 40 | >>> defaults = UserDefaults(filepath=filepath) 41 | >>> defaults.Set('key', 'value') 42 | >>> defaults.Save() 43 | 44 | If filepath points to an existing file, it will 45 | use that file and initializing a new config object 46 | by reading from it. 47 | 48 | If you later want to read in the config file: 49 | 50 | >>> defaults.Read() 51 | >>> print defaults.Get('setting') 52 | value 53 | >>> print defaults.Get('does-not-exist', default='use default instead') 54 | use default instead 55 | """ 56 | 57 | def __init__(self, filepath, defaults=None, header='Settings'): 58 | """ 59 | Initializes a new user defaults object by either reading or creating 60 | a settings file at 'filepath'. 61 | 62 | :param filepath: ``str`` 63 | usually ``res/settings.ini``relative to the 64 | source code file of the plugin, that uses the config store. 65 | :param defaults: ``dict`` default values to be used if the config 66 | file needs to be created. It is also possible to pass None 67 | here and then use the :py:func:`Set` and :py:func:`Save` functions 68 | to set inidividual settings after creation of the config object. 69 | :param header: ``str`` the name for a section in the .ini file. 70 | Usually you can get away with leaving it at the default. 71 | This will add a header "[Settings]" under which your 72 | settings will appear. If you have more advanced uses 73 | you are advised to modify the config parser state 74 | directly through ``self.state``. 75 | """ 76 | super(UserDefaults, self).__init__() 77 | if not isinstance(filepath, basestring): 78 | raise TypeError("E: param 'filepath': expected type 'str', but got %s" % type(filepath)) 79 | if not isinstance(header, basestring): 80 | raise TypeError("E: param 'header': expected type 'str', but got %s" % type(header)) 81 | if defaults and not isinstance(defaults, dict): 82 | raise TypeError("E: param 'defaults': expected type 'dict', but got %s" % type(defaults)) 83 | if defaults and not os.path.exists(filepath): 84 | # check if any values in defaults are not of type string 85 | # since ConfigParser needs string - always. 86 | _defaults = defaults.copy() 87 | for k, v in _defaults.iteritems(): 88 | if not isinstance(v, basestring): 89 | defaults[k] = str(v) 90 | config = ConfigParser.ConfigParser(defaults) 91 | else: 92 | config = ConfigParser.ConfigParser() 93 | self.state = config 94 | self.filepath = filepath 95 | self.header = header 96 | if os.path.exists(filepath): 97 | if DEBUG: 98 | print("reading existing settings file at path %r" % filepath) 99 | self.Read() 100 | else: 101 | self.state.add_section(header) 102 | self.Save(config, filepath) 103 | if DEBUG: 104 | print("saving new state %r to settings file at path %r" % (self.state, filepath)) 105 | 106 | def __str__(self, *args, **kwargs): 107 | return ("%r, header = %s, filepath = %s, state = %s" % 108 | (self, self.header, self.filepath, self.state)) 109 | 110 | def Get(self, name, section=None, default=None, valuetype="str"): 111 | """ 112 | Retrieve a previously stored value from the config object. 113 | 114 | :param str name: name of the setting 115 | :param str section: the section name. ``self.header`` if None. 116 | :param any default: a default value to use in case name wasn't found. 117 | :param str valuetype: type of the value to get. can be one of ``[str, bool, int, float]``. 118 | :return: ``valuetype`` on success, None or 'default' on failure. 119 | """ 120 | if section is None: 121 | section = self.header 122 | result = default 123 | try: 124 | if valuetype == "bool": 125 | result = self.state.getboolean(section, name) 126 | elif valuetype == "int": 127 | result = self.state.getint(section, name) 128 | elif valuetype == "float": 129 | result = self.state.getfloat(section, name) 130 | else: 131 | result = self.state.get(section, name) 132 | except ConfigParser.NoOptionError, noe: # IGNORE:W0612 @UnusedVariable 133 | pass 134 | except Exception, e: 135 | print("*** Caught exception while getting %r: %s" % (name, e)) 136 | return result 137 | 138 | def GetInt(self, name, section=None, default=None): 139 | """ 140 | Retrieve a previously stored integer value from the config object. 141 | 142 | :param str name: name of the setting 143 | :param str section: the section name. ``self.header`` if None. 144 | :param any default: a default value to use in case name wasn't found. 145 | :return: ``int`` on success, None or 'default' on failure. 146 | """ 147 | return self.Get(name, section=section, default=default, valuetype="int") 148 | 149 | def GetFloat(self, name, section=None, default=None): 150 | """ 151 | Retrieve a previously stored float value from the config object. 152 | 153 | :param str name: name of the setting 154 | :param str section: the section name. ``self.header`` if None. 155 | :param any default: a default value to use in case name wasn't found. 156 | :return: ``float`` on success, None or 'default' on failure. 157 | """ 158 | return self.Get(name, section=section, default=default, valuetype="float") 159 | 160 | def GetBool(self, name, section=None, default=None): 161 | """ 162 | Retrieve a previously stored boolean value from the config object. 163 | 164 | :param str name: name of the setting 165 | :param str section: the section name. ``self.header`` if None. 166 | :param any default: a default value to use in case name wasn't found. 167 | :return: ``bool`` on success, None or 'default' on failure. 168 | """ 169 | return self.Get(name, section=section, default=default, valuetype="bool") 170 | 171 | def Set(self, name, value, section=None): #@ReservedAssignment 172 | """ 173 | Store a value in the config object for later retrieval. 174 | 175 | :param name: ``str`` name of the setting 176 | :param value: ``any`` value to set. 177 | :param section: ``str`` the section name. ``self.header`` if None. 178 | :return: True if successful, False otherwise. 179 | """ 180 | if section is None: 181 | section = self.header 182 | result = False 183 | try: 184 | self.state.set(section, name, str(value)) 185 | result = True 186 | except Exception, e: # IGNORE:W0703 187 | print("*** Caught exception while setting %r to %r: %s" % (name, value, e)) 188 | return result 189 | 190 | def Read(self): 191 | """ 192 | Read state from configuration file. 193 | 194 | :return: True if successful or None, if config file couldn't be read. 195 | """ 196 | read_ok = self.state.read(self.filepath) 197 | if len(read_ok) == 0: 198 | if DEBUG: 199 | print("could not read config file at path %r" % self.filepath) 200 | return None 201 | return True 202 | 203 | def Save(self, config=None, filepath=None): 204 | """ 205 | Save settings to a configuration file. 206 | 207 | :param config: ``ConfigParser`` 208 | the config object to save. 209 | If None, uses ``self.config`` instead. 210 | :param filepath: ``str`` 211 | allows for specifying another path 212 | than ``self.filepath`` in order to save a copy 213 | of the config object. 214 | :return: True if successful, False otherwise. 215 | """ 216 | if filepath is None: 217 | filepath = self.filepath 218 | elif not isinstance(filepath, basestring): 219 | raise TypeError("E: param 'filepath': expected type 'str', but got %s" % type(filepath)) 220 | if config and not isinstance(config, ConfigParser.ConfigParser): 221 | raise TypeError("E: param 'config': expected type 'ConfigParser', but got %r" % config) 222 | else: 223 | config = self.state 224 | result = False 225 | try: 226 | with open(filepath, 'wb') as configfile: 227 | config.write(configfile) 228 | result = True 229 | except Exception, e: # IGNORE:W0703 230 | print("*** Caught exception while writing config: %r ***" % e) 231 | return result 232 | 233 | 234 | # Licensed under the Apache License, Version 2.0 (the "License"); 235 | # you may not use this file except in compliance with the License. 236 | # You may obtain a copy of the License at 237 | # 238 | # http://www.apache.org/licenses/LICENSE-2.0 239 | # 240 | # Unless required by applicable law or agreed to in writing, software 241 | # distributed under the License is distributed on an "AS IS" BASIS, 242 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 243 | # See the License for the specific language governing permissions and 244 | # limitations under the License. 245 | -------------------------------------------------------------------------------- /source/py4dlib/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # utils.py 4 | # py4dlib 5 | # 6 | # Created by André Berg on 2012-09-28. 7 | # Copyright 2012 Berg Media. All rights reserved. 8 | # 9 | # andre.bergmedia@googlemail.com 10 | # 11 | # pylint: disable-msg=F0401 12 | 13 | '''py4dlib.utils -- utility toolbelt for great convenience.''' 14 | 15 | import os 16 | import warnings 17 | 18 | __version__ = (0, 6) 19 | __date__ = '2012-09-27' 20 | __updated__ = '2013-08-12' 21 | 22 | 23 | DEBUG = 1 or ('DebugLevel' in os.environ and os.environ['DebugLevel'] > 0) 24 | TESTRUN = 0 or ('TestRunLevel' in os.environ and os.environ['TestRunLevel'] > 0) 25 | 26 | 27 | from subprocess import Popen, PIPE 28 | from functools import wraps, partial 29 | 30 | try: 31 | import c4d #@UnresolvedImport 32 | C4D_VERSION = c4d.GetC4DVersion() 33 | except ImportError: 34 | # TestRunLevel is defined by the Pydev PyUnit launcher to be 1 35 | if TESTRUN == 1: 36 | # define hard coded version here so test scripts 37 | # can set the desired version after importing py4dlib.utils 38 | C4D_VERSION = 12043 39 | 40 | 41 | def ClearConsole(): 42 | version = c4d.GetC4DVersion() 43 | if version > 12999: 44 | cmd = 13957 # R14 (and R13?) 45 | else: 46 | cmd = 1024314 # R12 47 | c4d.CallCommand(cmd) 48 | 49 | 50 | def FuzzyCompareStrings(a, b, limit=20): 51 | """ Fuzzy string comparison. 52 | 53 | Two strings are deemed equal if they have 54 | the same byte sequence for up to 'limit' chars. 55 | 56 | Limit can be an int or a percentage string 57 | like for example '60%' in which case 2 strings 58 | are deemed equal if at least '60%' relative to 59 | the longest string match. 60 | """ 61 | result = True 62 | maxchars = limit 63 | if isinstance(limit, basestring): 64 | try: 65 | maxp = int(limit[:-1]) # chop off '%' 66 | maxlen = max(len(a), len(b)) # percentage relative to longest str 67 | maxchars = int((maxp/100.0) * maxlen) 68 | except: 69 | raise ValueError("E: param 'limit' must be one of [str, int] " + 70 | "where str indicates a percentage, e.g. '75%'.") 71 | idx = 0 72 | for char in a: 73 | if idx >= maxchars: 74 | break 75 | try: 76 | if char != b[idx]: 77 | result = False 78 | except IndexError: 79 | result = False 80 | idx += 1 81 | return result 82 | 83 | 84 | def EscapeUnicode(s): 85 | ur""" CINEMA 4D R12's CPython integration stores high-order chars (``ord > 126``) 86 | as 4-byte unicode escape sequences with upper case hex letters. 87 | 88 | For example the character ``ä`` (LATIN SMALL LETTER A WITH DIAERESIS) 89 | would be stored as the byte sequence ``\u00E4``. This function replaces 90 | high-order chars with a unicode escape sequence suitable for CINEMA 4D. 91 | 92 | If you use this function in R12 you need to balance each call with a 93 | call to :py:func:`UnescapeUnicode` when the time comes to use or display 94 | the string. 95 | 96 | In R13 and R14 it returns the string untouched since in those versions 97 | the CPython intergration handles Unicode encoded strings properly. 98 | """ 99 | result = "" 100 | if C4D_VERSION <= 12999: 101 | try: 102 | s = s.decode('utf-8').encode('latin-1') 103 | except UnicodeDecodeError: 104 | pass 105 | except UnicodeEncodeError: 106 | pass 107 | for b in s: 108 | if ord(b) > 126: 109 | result += r"\u%04X" % (ord(b),) 110 | else: 111 | result += b 112 | if DEBUG: 113 | print("result = %r" % result) 114 | else: 115 | return s 116 | return result 117 | 118 | 119 | def UnescapeUnicode(s): 120 | ur""" CINEMA 4D R12's CPython integration stores high-order chars (``ord > 126``) 121 | as 4-byte unicode escape sequences with upper case hex letters. 122 | 123 | This function converts unicode escape sequences used by CINEMA 4D when passing 124 | bytes (e.g. ``\u00FC`` -> ``\xfc``) to their corresponding high-order characters. 125 | 126 | It should be used in R12 only and should balance out any calls made to 127 | :py:func:`EscapeUnicode`. 128 | 129 | In R13 and R14 the string is returned untouched since in those versions 130 | the CPython intergration handles Unicode encoded strings properly. 131 | """ 132 | if C4D_VERSION <= 12999: 133 | try: 134 | return s.decode('unicode_escape') 135 | except UnicodeEncodeError: 136 | return s 137 | else: 138 | try: 139 | return s.decode("utf-8") 140 | except UnicodeDecodeError: 141 | return s 142 | except UnicodeEncodeError: 143 | return s 144 | 145 | 146 | def VersionString(versionTuple): 147 | """ (x,y,z .. n) -> 'x.y.z...n' """ 148 | return '.'.join(str(x) for x in versionTuple) 149 | 150 | 151 | def PPLLString(ll): 152 | """ Returns a pretty-printed string of a ``list`` structure. """ 153 | s = " " + repr(ll)[1:-2] 154 | lines = s.split('],') 155 | result = '],\n'.join(lines) 156 | return result + ']' 157 | 158 | 159 | def System(cmd, args=None): 160 | ''' 161 | Convenience function for firing off commands to 162 | the System console. Used instead of `subprocess.call`_ 163 | so that shell variables will be expanded properly. 164 | 165 | Not the same as `os.system`_ as here it captures 166 | returns ``stdout`` and ``stderr`` in a tuple in 167 | Python 2.5 and lower or a ``namedtuple`` in 2.6 168 | and higher. So you can use ``result[0]`` in the 169 | first case and ``result.out`` in the second. 170 | 171 | :param cmd: a console command line 172 | :type cmd: ``string`` 173 | :param args: a list of arguments that 174 | will be expanded in cmd 175 | starting with ``$0`` 176 | :type args: ``list`` 177 | :return: ``tuple`` or ``namedtuple`` 178 | ''' 179 | if args is None: 180 | fullcmd = cmd 181 | else: 182 | args = ["'{}'".format(s.replace(r'\\', r'\\\\') 183 | .replace("'", r"\'")) for s in args] 184 | fullcmd = "%s %s" % (cmd, ' '.join(args)) 185 | out, err = Popen(fullcmd, stdout=PIPE, shell=True).communicate() 186 | System.out = out 187 | System.err = err 188 | try: 189 | from collections import namedtuple 190 | StdStreams = namedtuple('StdStreams', ['out', 'err']) 191 | return StdStreams(out=out, err=err) 192 | except ImportError: 193 | return (out, err) 194 | 195 | 196 | def benchmark(func=None, prec=3, unit='auto', name_width=0, time_width=8): 197 | """ 198 | A decorator that prints the time a function takes 199 | to execute per call and cumulative total. 200 | 201 | Accepts the following keyword arguments: 202 | 203 | :param unit: ``str`` time unit for display. one of `[auto, us, ms, s, m]`. 204 | :param prec: ``int`` radix point precision. 205 | :param name_width: ``int`` width of the right-aligned function name field. 206 | :param time_width: ``int`` width of the right-aligned time value field. 207 | 208 | For convenience you can also set attributes on the benchmark 209 | function itself with the same name as the keyword arguments 210 | and the value of those will be used instead. This saves you 211 | from having to call the decorator with the same arguments each 212 | time you use it. Just set, for example, ``benchmark.prec = 5`` 213 | after the import and before you use it for the first time. 214 | """ 215 | import time 216 | if hasattr(benchmark, 'prec'): 217 | prec = getattr(benchmark, 'prec') 218 | if hasattr(benchmark, 'unit'): 219 | unit = getattr(benchmark, 'unit') 220 | if hasattr(benchmark, 'name_width'): 221 | name_width = getattr(benchmark, 'name_width') 222 | if hasattr(benchmark, 'time_width'): 223 | time_width = getattr(benchmark, 'time_width') 224 | if func is None: 225 | return partial(benchmark, prec=prec, unit=unit, 226 | name_width=name_width, time_width=time_width) 227 | @wraps(func) 228 | def wrapper(*args, **kwargs): # IGNORE:W0613 229 | def _get_unit_mult(val, unit): 230 | multipliers = {'us': 1000000.0, 'ms': 1000.0, 's': 1.0, 'm': (1.0 / 60.0)} 231 | if unit in multipliers: 232 | mult = multipliers[unit] 233 | else: # auto 234 | if val >= 60.0: 235 | unit = "m" 236 | elif val >= 1.0: 237 | unit = "s" 238 | elif val <= 0.001: 239 | unit = "us" 240 | else: 241 | unit = "ms" 242 | mult = multipliers[unit] 243 | return (unit, mult) 244 | t = time.clock() 245 | res = func(*args, **kwargs) 246 | td = (time.clock() - t) 247 | wrapper.total += td 248 | wrapper.count += 1 249 | tt = wrapper.total 250 | cn = wrapper.count 251 | tdu, tdm = _get_unit_mult(td, unit) 252 | ttu, ttm = _get_unit_mult(tt, unit) 253 | td *= tdm 254 | tt *= ttm 255 | print(" -> {0:>{8}}() @ {1:>03}: {3:>{7}.{2}f} {4:>2}, total: {5:>{7}.{2}f} {6:>2}" 256 | .format(func.__name__, cn, prec, td, tdu, tt, ttu, time_width, name_width)) 257 | return res 258 | wrapper.total = 0 259 | wrapper.count = 0 260 | return wrapper 261 | 262 | 263 | def require(*args, **kwargs): 264 | ''' 265 | Decorator that enforces types for function/method args. 266 | 267 | Two ways to specify which types are required for each arg. 268 | 269 | 1) 2-tuples, where first member specifies arg index or arg name, 270 | second member specifies a type or a tuple of types. 271 | 2) kwargs style, e.g. `argname`=`types` where `types` again can 272 | be a type or a tuple of types. 273 | 274 | None is always a valid type, to allow for optional args. 275 | ''' 276 | _required = [] 277 | _args = args 278 | _kwargs = kwargs 279 | def wrapper(func): 280 | if hasattr(func, "wrapped_args"): 281 | wrapped_args = getattr(func, "wrapped_args") 282 | else: 283 | code = func.func_code 284 | wrapped_args = list(code.co_varnames[:code.co_argcount]) 285 | @wraps(func) 286 | def wrapped_fn(*args, **kwargs): 287 | def _check_type(_funcname, _locator, _arg, _types): 288 | if _arg is not None and not isinstance(_arg, _types): 289 | pluralstr = "one " if isinstance(_types, (list, tuple)) else "" # IGNORE:W0311 290 | raise TypeError("E: for %s(): param %r must be %sof %r, but is %r" % # IGNORE:W0311 291 | (_funcname, str(_locator), pluralstr, _types, type(_arg))) 292 | def _get_index(param_name): 293 | codeobj_varnames = wrapped_args 294 | try: 295 | _idx = codeobj_varnames.index(param_name) 296 | return _idx 297 | except ValueError: 298 | raise NameError(param_name) 299 | for i in _args: 300 | locator = i[0] 301 | types = i[1] 302 | idx = None 303 | if isinstance(locator, int): 304 | idx = locator 305 | elif isinstance(locator, basestring): 306 | name = locator 307 | idx = _get_index(name) 308 | if name in kwargs: 309 | _check_type(func.__name__, name, kwargs[name], types) 310 | if idx >= len(args): 311 | break 312 | _check_type(func.__name__, idx, args[idx], types) 313 | for k, v in _kwargs.iteritems(): 314 | name = k 315 | types = v 316 | if name in kwargs: 317 | _check_type(func.__name__, name, kwargs[name], types) 318 | else: 319 | idx = _get_index(name) 320 | if idx >= len(args): 321 | break 322 | _check_type(func.__name__, name, args[idx], types) 323 | return func(*args, **kwargs) 324 | wrapped_fn.wrapped_args = wrapped_args 325 | return wrapped_fn 326 | return wrapper 327 | 328 | 329 | def deprecated(level=1, since=None, info=None): 330 | """This decorator can be used to mark functions as deprecated. 331 | 332 | :param int level: severity level. 333 | 0 = warnings.warn(category=DeprecationWarning) 334 | 1 = warnings.warn_explicit(category=DeprecationWarning) 335 | 2 = raise DeprecationWarning() 336 | :param string since: the version where deprecation was introduced. 337 | :param string info: additional info. normally used to refer to the new 338 | function now favored in place of the deprecated one. 339 | """ 340 | def __decorate(func): 341 | if since is None: 342 | msg = 'Method %s() is deprecated.' % func.__name__ 343 | else: 344 | msg = 'Method %s() has been deprecated since version %s.' % (func.__name__, str(since)) 345 | if info: 346 | msg += ' ' + info 347 | @wraps(func) 348 | def __wrapped(*args, **kwargs): # IGNORE:C0111 349 | if level <= 0: 350 | warnings.warn(msg, category=DeprecationWarning, stacklevel=2) 351 | func(*args, **kwargs) 352 | elif level == 1: 353 | warnings.warn_explicit(msg, category=DeprecationWarning, 354 | filename=func.func_code.co_filename, 355 | lineno=func.func_code.co_firstlineno + 1) 356 | elif level >= 2: 357 | raise DeprecationWarning(msg) 358 | return __wrapped 359 | return __decorate 360 | 361 | 362 | def cache(func): 363 | """Classic cache decorator.""" 364 | saved = {} 365 | @wraps(func) 366 | def wrapped_fn(*args): 367 | if args in saved: 368 | return saved[args] 369 | result = func(*args) 370 | saved[args] = result 371 | return result 372 | return wrapped_fn 373 | 374 | 375 | def memoize(func): 376 | """Classic memoization decorator.""" 377 | mem_cache = {} 378 | @wraps(func) 379 | def wrapped_fn(*args, **kw): 380 | key = (args, tuple(sorted(kw.items()))) 381 | if key not in mem_cache: 382 | mem_cache[key] = func(*args, **kw) 383 | return mem_cache[key] 384 | return wrapped_fn 385 | 386 | 387 | # Licensed under the Apache License, Version 2.0 (the "License"); 388 | # you may not use this file except in compliance with the License. 389 | # You may obtain a copy of the License at 390 | # 391 | # http://www.apache.org/licenses/LICENSE-2.0 392 | # 393 | # Unless required by applicable law or agreed to in writing, software 394 | # distributed under the License is distributed on an "AS IS" BASIS, 395 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 396 | # See the License for the specific language governing permissions and 397 | # limitations under the License. 398 | -------------------------------------------------------------------------------- /source/py4dlib/examples/Extract Polys 1.0/Extract.pyp: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Extract.pyp 4 | # py4dlib.examples 5 | # 6 | # Created by André Berg on 2013-08-02. 7 | # Copyright 2013 Berg Media. All rights reserved. 8 | # 9 | # andre.bergmedia@googlemail.com 10 | # 11 | # pylint: disable-msg=F0401,E1101,W0232 12 | 13 | ''' 14 | Extract -- extracts selected polygons into a new polygon object. 15 | 16 | Summary: 17 | 18 | Same as selecting polygons on some objects, 19 | running Split, then deleting the inverse 20 | selection to keep only the selected polys. 21 | ''' 22 | 23 | import os 24 | import time 25 | 26 | __all__ = [] 27 | __version__ = (1, 0) 28 | __date__ = '2013-08-02' 29 | __updated__ = '2013-08-02' 30 | 31 | 32 | DEBUG = 0 or ('DebugLevel' in os.environ and os.environ['DebugLevel'] > 0) 33 | TESTRUN = 0 or ('TestRunLevel' in os.environ and os.environ['TestRunLevel'] > 0) 34 | 35 | PY4DLIB_FOUND = False 36 | 37 | if DEBUG: 38 | import pprint 39 | pp = pprint.PrettyPrinter(width=200) 40 | PP = pp.pprint 41 | PF = pp.pformat 42 | 43 | try: 44 | import c4d #@UnresolvedImport 45 | from c4d import plugins, bitmaps, gui, documents 46 | except ImportError: 47 | if TESTRUN == 1: 48 | pass 49 | 50 | try: 51 | from py4dlib.objects import DeselectAll, Select, GetGlobalRotation 52 | from py4dlib.objects import SetGlobalRotation, SetAxisRotation 53 | from py4dlib.objects import SelectObjects, InsertUnderNull 54 | from py4dlib.mesh import TogglePolySelection, GetSelectedPolys 55 | PY4DLIB_FOUND = True 56 | except ImportError: 57 | pass 58 | 59 | 60 | # ---------------------------------------------- 61 | # GLOBALS 62 | # ---------------------------------------------- 63 | 64 | CR_YEAR = time.strftime("%Y") 65 | 66 | PLUGIN_VERSION = '.'.join(str(x) for x in __version__) 67 | PLUGIN_NAME = "Extract" 68 | PLUGIN_HELP = "Extracts polygon selections from a model, where contiguous surfaces are scattered over multiple polygon objects." 69 | PLUGIN_ABOUT = """(C) %s Andre Berg (Berg Media) 70 | All rights reserved. 71 | 72 | Version %s 73 | 74 | Same as selecting polygons on some objects, 75 | running Split, then deleting the inverse 76 | selection to keep only the selected polys. 77 | Plus a few extras. Works with multiple object 78 | selections. 79 | 80 | Use at your own risk! 81 | 82 | It is recommended to try out the plugin 83 | on a spare copy of your data first. 84 | """ % (CR_YEAR, PLUGIN_VERSION) 85 | 86 | PY4DLIB_NOT_FOUND_MSG = """This plugin needs py4dlib which is missing. 87 | 88 | Please download and install it free of charge 89 | from http://github.com/andreberg/py4dlib. 90 | """ 91 | 92 | # ------------------------------------------- 93 | # PLUGING IDS 94 | # ------------------------------------------- 95 | 96 | # unique ID 97 | ID_EXTRACT = 1026898 98 | 99 | # Element IDs 100 | IDD_DIALOG_SETTINGS = 10001 101 | IDC_BUTTON_CANCEL = 10002 102 | IDC_BUTTON_DOIT = 10003 103 | IDC_GROUP_WRAPPER = 10004 104 | IDC_GROUP_SETTINGS = 10005 105 | IDC_GROUP_BUTTONS = 10006 106 | IDC_DUMMY = 10007 107 | IDC_CHK_OPTIMIZE = 10008 108 | IDC_CHK_CENTER = 10009 109 | IDC_CHK_KEEPNORMALTAG = 10010 110 | IDC_CHK_HIDE = 10011 111 | IDC_CHK_DELETEORIG = 10012 112 | IDC_CHK_RENORMALIZE = 10013 113 | IDC_MENU_ABOUT = 30001 114 | 115 | # String IDs 116 | IDS_DIALOG_TITLE = PLUGIN_NAME 117 | IDS_MENU_INFO = "Info" 118 | IDS_MENU_ABOUT = "About..." 119 | 120 | 121 | # ------------------------------------------------------ 122 | # User Interface 123 | # ------------------------------------------------------ 124 | 125 | class ExtractDialog(gui.GeDialog): 126 | 127 | def CreateLayout(self): 128 | plugins.GeResource().Init(os.path.dirname(os.path.abspath(__file__))) 129 | self.LoadDialogResource(IDD_DIALOG_SETTINGS, flags=c4d.BFH_SCALEFIT) 130 | 131 | # Menu 132 | self.MenuFlushAll() 133 | self.MenuSubBegin(IDS_MENU_INFO) 134 | self.MenuAddString(IDC_MENU_ABOUT, IDS_MENU_ABOUT) 135 | self.MenuSubEnd() 136 | 137 | self.MenuFinished() 138 | 139 | self.SetTitle(IDS_DIALOG_TITLE) 140 | 141 | return True 142 | 143 | def InitValues(self): 144 | deleteorig = False 145 | keepnormal = False 146 | self.SetBool(IDC_CHK_OPTIMIZE, True) 147 | self.SetBool(IDC_CHK_CENTER, True) 148 | self.SetBool(IDC_CHK_DELETEORIG, deleteorig) 149 | self.SetBool(IDC_CHK_HIDE, False) 150 | self.SetBool(IDC_CHK_KEEPNORMALTAG, keepnormal) 151 | self.SetBool(IDC_CHK_RENORMALIZE, False) 152 | 153 | self.Enable(IDC_CHK_RENORMALIZE, keepnormal) 154 | self.Enable(IDC_CHK_HIDE, deleteorig) 155 | return True 156 | 157 | def Disable(self, ID): 158 | return self.Enable(ID, False) 159 | 160 | def Command(self, ID, msg): 161 | if ID == IDC_BUTTON_DOIT: 162 | scriptvars = { 163 | 'optimize': self.GetBool(IDC_CHK_OPTIMIZE), 164 | 'nodelete': self.GetBool(IDC_CHK_HIDE), 165 | 'centeraxis': self.GetBool(IDC_CHK_CENTER), 166 | 'keepnormaltags': self.GetBool(IDC_CHK_KEEPNORMALTAG), 167 | 'renormalize': self.GetBool(IDC_CHK_RENORMALIZE), 168 | 'deleteorig': self.GetBool(IDC_CHK_DELETEORIG) 169 | } 170 | script = ExtractScript(scriptvars) 171 | if DEBUG: 172 | print("do it: %r" % msg) 173 | print("script = %r" % script) 174 | print("scriptvars = %r" % scriptvars) 175 | try: 176 | return script.run() 177 | except Exception as e: 178 | if DEBUG or TESTRUN: 179 | print(e) 180 | finally: 181 | c4d.StatusClear() 182 | elif ID == IDC_BUTTON_CANCEL: 183 | if DEBUG: 184 | print("cancel: %r" % msg) 185 | self.Close() 186 | elif ID == IDC_MENU_ABOUT: 187 | c4d.gui.MessageDialog(PLUGIN_ABOUT) 188 | elif ID == IDC_CHK_DELETEORIG: 189 | self.Enable(IDC_CHK_HIDE, (self.GetBool(IDC_CHK_DELETEORIG))) 190 | elif ID == IDC_CHK_KEEPNORMALTAG: 191 | self.Enable(IDC_CHK_RENORMALIZE, self.GetBool(IDC_CHK_KEEPNORMALTAG)) 192 | else: 193 | if DEBUG: 194 | print("ID = %s" % ID) 195 | 196 | return True 197 | 198 | 199 | 200 | # ------------------------------------------------------ 201 | # Command Script 202 | # ------------------------------------------------------ 203 | 204 | class ExtractScript(object): 205 | 206 | def __init__(self, scriptvars=None): 207 | super(ExtractScript, self).__init__() 208 | self.data = scriptvars 209 | 210 | def hideDelete(self, objs, doc): 211 | # Hide/Delete 212 | if objs is None: 213 | raise ValueError("objs can't be None") 214 | if not isinstance(objs, list): 215 | objs = [objs] 216 | settings = c4d.BaseContainer() 217 | if self.data['nodelete'] == True: 218 | kmd = c4d.MCOMMAND_HIDESELECTED 219 | verb = "hide selected" 220 | else: 221 | for obj in objs: 222 | doc.AddUndo(c4d.UNDOTYPE_CHANGE, obj) 223 | kmd = c4d.MCOMMAND_DELETE 224 | verb = "delete" 225 | retval = c4d.utils.SendModelingCommand( 226 | command=kmd, list=objs, 227 | mode=c4d.MODIFY_POLYGONSELECTION, 228 | bc=settings, doc=doc 229 | ) 230 | if DEBUG: 231 | if retval is False: 232 | print("%s failed." % verb) 233 | elif retval is True: 234 | print("%s successful." % verb) 235 | return retval 236 | 237 | def run(self): 238 | if not PY4DLIB_FOUND: 239 | c4d.gui.MessageDialog(PY4DLIB_NOT_FOUND_MSG) 240 | return False 241 | 242 | doc = documents.GetActiveDocument() 243 | doc.StartUndo() 244 | 245 | sel = doc.GetSelection() 246 | if sel is None or len(sel) == 0: 247 | return False 248 | 249 | for op in sel: 250 | if not isinstance(op, c4d.PolygonObject): 251 | c4d.gui.MessageDialog("Please make sure your selection includes polygon objects only.") 252 | return False 253 | 254 | c4d.StatusSetSpin() 255 | timestart = c4d.GeGetMilliSeconds() 256 | 257 | grps = [] 258 | cpys = [] 259 | i = 0 # IGNORE:W0612 260 | 261 | c4d.StopAllThreads() 262 | DeselectAll(True) 263 | 264 | # loop through all objects 265 | for op in sel: 266 | polysel = GetSelectedPolys(op) 267 | if len(polysel) == 0: 268 | print("skipping %s because no polygon selection" % op.GetName()) 269 | continue 270 | 271 | cpy = op.GetClone() 272 | doc.AddUndo(c4d.UNDOTYPE_NEW, cpy) 273 | doc.InsertObject(cpy) 274 | cpys.append(cpy) 275 | 276 | if self.data['deleteorig']: 277 | self.hideDelete(op, doc) 278 | 279 | Select(cpy) 280 | 281 | # unfortunately we need to kill the normal tags 282 | # since the points get shuffled 283 | if self.data['keepnormaltags'] == False: 284 | cpy.KillTag(c4d.Tnormal) 285 | 286 | # now deselect selected poly and select unselected 287 | TogglePolySelection(cpy) 288 | modobjs = [] 289 | 290 | retval = self.hideDelete([cpy], doc) 291 | if isinstance(retval, list): 292 | modobjs.extend(retval) 293 | elif isinstance(retval, c4d.BaseObject): 294 | modobjs.append(retval) 295 | elif retval is False: 296 | return False 297 | 298 | # Optimize Unused Points 299 | settings = c4d.BaseContainer() 300 | if self.data['optimize'] == True: 301 | settings[c4d.MDATA_OPTIMIZE_TOLERANCE] = 0.1 302 | settings[c4d.MDATA_OPTIMIZE_POINTS] = False 303 | settings[c4d.MDATA_OPTIMIZE_POLYGONS] = False 304 | settings[c4d.MDATA_OPTIMIZE_UNUSEDPOINTS] = True 305 | retval = c4d.utils.SendModelingCommand( 306 | command=c4d.MCOMMAND_OPTIMIZE, list=[op, cpy], 307 | mode=c4d.MODIFY_POLYGONSELECTION, 308 | bc=settings, doc=doc 309 | ) 310 | if retval is False: 311 | if DEBUG: print("optimize failed") 312 | return False 313 | elif retval is True: 314 | if DEBUG: print("optimize successful") 315 | 316 | if DEBUG: 317 | print("modobjs = %r" % modobjs) 318 | 319 | # Center cpy axis 320 | if self.data['centeraxis'] == True: 321 | c4d.CallCommand(1011982) # center axis to... 322 | c4d.EventAdd() 323 | 324 | i += 1 325 | DeselectAll(True) 326 | # end for 327 | 328 | if i == 0: 329 | print("Nothing to do...") 330 | return False 331 | 332 | # Create collecting group 333 | op = sel[0] 334 | try: 335 | parent = op.GetUp() 336 | parentname = parent.GetName() 337 | prot = GetGlobalRotation(parent) 338 | except Exception: # IGNORE:W0703 339 | parent = None 340 | parentname = "Objects" 341 | prot = c4d.Vector(0,0,0) 342 | grpname = "Detached %s" % parentname 343 | 344 | # get parent pos and rot (if applicable) 345 | pmg = op.GetUpMg() 346 | 347 | # Add cpy to group 348 | SelectObjects(cpys) 349 | grp = InsertUnderNull(cpys, grp=None, name=grpname) 350 | gmg = grp.GetMg() 351 | 352 | if DEBUG: 353 | print("pmg = %r" % pmg) 354 | print("gmg = %r" % gmg) 355 | 356 | # calc current offset to world 0 357 | # (since our new copies are created 358 | # relative to world zero), then add 359 | # the difference to pmg and use the 360 | # result as new pos for grp 361 | wdiff = gmg.off - c4d.Vector(0,0,0) 362 | newoff = pmg.off + wdiff 363 | gmg.off = newoff 364 | 365 | if DEBUG: 366 | print("wdiff = %r" % wdiff) 367 | print("newoff = %r" % newoff) 368 | print("gmg = %r" % gmg) 369 | 370 | grp.SetMg(gmg) 371 | if parent is not None: # if it has a parent 372 | SetGlobalRotation(grp, prot) 373 | 374 | if (self.data['keepnormaltags'] == True and 375 | self.data['renormalize'] == True): 376 | # since CINEMA 4D performs object axis 377 | # transform ("Y Up") when importing OBJ files 378 | # we need to recreate this manually for the 379 | # normal data to make sense again, e.g. 380 | # rotate the object axis by B=90 381 | for op in grp.GetChildren(): 382 | if op.GetTag(c4d.Tnormal) is not None: 383 | theta = c4d.utils.Rad(90) 384 | trot = c4d.Vector(0, 0, theta) 385 | SetAxisRotation(op, trot - prot) 386 | 387 | if DEBUG: 388 | print("grp = %r" % grp) 389 | print("grps = %r" % grps) 390 | print("cpys = %r" % cpys) 391 | 392 | doc.SetMode(c4d.Mmodel) 393 | 394 | c4d.StatusClear() 395 | 396 | # tell C4D to update internal state 397 | c4d.EventAdd() 398 | doc.EndUndo() 399 | 400 | timeend = int(c4d.GeGetMilliSeconds() - timestart) 401 | timemsg = "Extract: finished in " + str(timeend) + " milliseconds" 402 | print(timemsg) 403 | 404 | return True 405 | 406 | 407 | # ---------------------------------------------------- 408 | # Main 409 | # ---------------------------------------------------- 410 | class ExtractMain(plugins.CommandData): 411 | 412 | dialog = None 413 | 414 | def Execute(self, doc): # IGNORE:W0613 415 | # create the dialog 416 | if self.dialog is None: 417 | self.dialog = ExtractDialog() 418 | return self.dialog.Open(c4d.DLG_TYPE_ASYNC, pluginid=ID_EXTRACT, defaultw=230) 419 | 420 | def RestoreLayout(self, secref): 421 | # manage nonmodal dialog 422 | if self.dialog is None: 423 | self.dialog = ExtractDialog() 424 | return self.dialog.Restore(pluginid=ID_EXTRACT, secret=secref) 425 | 426 | 427 | 428 | if __name__ == "__main__": 429 | thispath = os.path.dirname(os.path.abspath(__file__)) 430 | icon = bitmaps.BaseBitmap() 431 | icon.InitWith(os.path.join(thispath, "res/", "icon.tif")) 432 | plugins.RegisterCommandPlugin( 433 | ID_EXTRACT, 434 | PLUGIN_NAME, 435 | 0, 436 | icon, 437 | PLUGIN_HELP, 438 | ExtractMain() 439 | ) 440 | print("%s v%s loaded. (C) %s Andre Berg" % (PLUGIN_NAME, PLUGIN_VERSION, CR_YEAR)) 441 | 442 | 443 | # Licensed under the Apache License, Version 2.0 (the "License"); 444 | # you may not use this file except in compliance with the License. 445 | # You may obtain a copy of the License at 446 | # 447 | # http://www.apache.org/licenses/LICENSE-2.0 448 | # 449 | # Unless required by applicable law or agreed to in writing, software 450 | # distributed under the License is distributed on an "AS IS" BASIS, 451 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 452 | # See the License for the specific language governing permissions and 453 | # limitations under the License. 454 | --------------------------------------------------------------------------------