├── .gitignore ├── LICENSE ├── README.md └── mdiff ├── __init__.py ├── api.py ├── core.py └── version.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 David Lai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
An ID renew logic for modification tracking in Maya pipeline,
that able to tell you which node is untracked and need identity update on demand.
(require node hashing for change detection)
This is a proof of concept, currently testing in production
7 | 8 | 9 | ### Motivation 10 | 11 | When using custom ID on nodes for mapping with other nodes or data that need to connect with or apply to, might require to ensure the node IDs in one artist's scene are consisted with or tracked in pipeline. That wasn't easy, especially after file-import or node's duplication happened. 12 | 13 | Therefore, I want to find a way to answer the following questions: 14 | 15 | 1. These nodes shared same ID, but which node is original ? or duplicated ? 16 | 2. Has this node been modified ? 17 | 3. Does this node have to renew it's ID ? 18 | 19 | 20 | ### Implementation 21 | 22 | First, I need to rephrase `Custom ID` to `address`, since it's an ID for lookup in the scene when mapping changes back to nodes. 23 | 24 | And here's what we will have in each tracked node: 25 | 26 | * UUID 27 | 28 | In each node we have a Maya UUID by default, UUID will stay the same when the node is a referenced node, but not imported or duplicated. 29 | 30 | * Address 31 | 32 | A pipeline custom ID for node, we will use a timestamp embedded ID, `uuid1` or `bson.ObjectId`. 33 | 34 | * Verifier 35 | 36 | This is a hash value generated from hashing `UUID` and `Address`, for us to recognize duplicated node, no matter the node is imported or referenced. 37 | 38 | * Fingerprint 39 | 40 | This is a hash value that need to be generated from **custom node hasher** *(not included in `mdiff`)*. For mesh type nodes, you may generate from hashing every vertices position, normals and UVs. For shader, you may generate from hashing every attribute value with the shader type name, even with texture file. *The point is, you need to produce a string value that able to help `mdiff` to identify changes that you want to be awared of*. 41 | 42 | Now, we can answer those three questions: 43 | 44 | 1. **These nodes shared same ID, but which node is original ? or duplicated ?** 45 | 46 | By hashing `uuid` and `address` again, and compare the value with `verifier`, if it's not equal, then this is a duplicated node. This will work is because the Maya UUID will change when the node get duplicated, thus the hash value of `uuid` and `address` will change, too. (To work on imported nodes, we need to generate `verifier` right after the nodes get imported.) 47 | 48 | 2. **Has this node been modified ?** 49 | 50 | If you have your custom node hasher ready, then this one is easy. Just hash the node again and compare the value with `fingerprint` attribute. 51 | 52 | 3. **Does this node have to renew it's ID ?** 53 | 54 | * No, if 55 | * Node is original and not changed 56 | * Node is duplicated and not changed 57 | * Node has been changed but is original 58 | * Yes, if 59 | * Node is newly created 60 | * Node is duplicated and has been changed 61 | 62 | 63 | ### Demo 64 | 65 | A simple walkthrough 66 | 67 | ##### Preparation 68 | 69 | ```python 70 | import maya.cmds as cmds 71 | import mdiff.api 72 | 73 | # Make a simple transfrom matrix hasher 74 | matrix_hasher = (lambda node: str(cmds.xform(node, query=True, matrix=True))) 75 | 76 | ``` 77 | 78 | ##### Create a new asset 79 | 80 | ```python 81 | node = cmds.polyCube(name="origin")[0] 82 | # Hash it 83 | fingerprint = matrix_hasher(node) 84 | # Since this is a new node, renew is a must 85 | state = mdiff.api.status(node, fingerprint) 86 | assert state == mdiff.api.Untracked 87 | # Register 88 | mdiff.api.on_track(node, fingerprint) 89 | 90 | ``` 91 | 92 | ##### Change 93 | 94 | ```python 95 | # Move the node around 96 | cmds.setAttr(node + ".tx", 5) 97 | # Hash it 98 | fingerprint = matrix_hasher(node) 99 | # Check 100 | state = mdiff.api.status(node, fingerprint) 101 | assert state == mdiff.api.Changed 102 | # it's been changed but is original, update fingerprint 103 | mdiff.api.on_change(node, fingerprint) 104 | 105 | ``` 106 | 107 | ##### Duplicate 108 | 109 | ```python 110 | clone = cmds.duplicate(node, name="clone")[0] 111 | # Check 112 | fingerprint = matrix_hasher(clone) 113 | state = mdiff.api.status(clone, fingerprint) 114 | assert state == mdiff.api.Duplicated 115 | # Although it's not changed but is a duplicate, update verifier 116 | mdiff.api.on_duplicate(clone) 117 | 118 | ``` 119 | 120 | ##### Duplicate & Change 121 | 122 | ```python 123 | rogue = cmds.duplicate(node, name="rogue")[0] 124 | # Now move the rogue ! 125 | cmds.setAttr(rogue + ".ty", 10) 126 | # Check 127 | fingerprint = matrix_hasher(rogue) 128 | state = mdiff.api.status(rogue, fingerprint) 129 | assert state == mdiff.api.Untracked 130 | # it's been changed and is a duplicate, need to renew ! 131 | mdiff.api.on_track(rogue, fingerprint) 132 | 133 | ``` 134 | 135 | To simplify, you can use `api.manage` 136 | 137 | ```python 138 | foo = "A node that you don't need to overwatch" 139 | fingerprint = my_hasher(foo) 140 | state = mdiff.api.status(foo, fingerprint) 141 | # Auto update node by state 142 | mdiff.api.manage(foo, fingerprint, state) 143 | 144 | ``` 145 | 146 | 147 | ### Example Usage 148 | 149 | ##### On publish or save 150 | ```python 151 | import mdiff.api 152 | 153 | for node in nodes: 154 | # Make fingerprint 155 | fingerprint = my_hasher(node) 156 | # Check status 157 | state = mdiff.api.status(node, fingerprint) 158 | # Auto management 159 | mdiff.api.manage(node, fingerprint, state) 160 | 161 | ``` 162 | 163 | OR 164 | 165 | ```python 166 | import mdiff.api 167 | 168 | for node in nodes: 169 | # make fingerprint 170 | fingerprint = my_hasher(node) 171 | # Check status 172 | state = mdiff.api.status(node, fingerprint) 173 | # Manual management 174 | if state == mdiff.api.Untracked: 175 | ... # do something 176 | mdiff.api.on_track(node, fingerprint) 177 | 178 | elif state == mdiff.api.Changed: 179 | ... # do something 180 | mdiff.api.on_change(node, fingerprint) 181 | 182 | elif state == mdiff.api.Duplicated: 183 | ... # do something 184 | mdiff.api.on_duplicate(node) 185 | 186 | ``` 187 | 188 | ##### On import 189 | ```python 190 | import mdiff.api 191 | 192 | nodes = cmds.file("path/to/scene", i=True, returnNewNodes=True) 193 | # Update identity on Maya UUID changed 194 | mdiff.api.update_verifiers(nodes) 195 | 196 | ``` 197 | -------------------------------------------------------------------------------- /mdiff/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from . import api 3 | 4 | from .version import ( 5 | version, 6 | version_info, 7 | __version__ 8 | ) 9 | 10 | 11 | __all__ = ( 12 | "api", 13 | "__version__", 14 | "version", 15 | "version_info", 16 | ) 17 | -------------------------------------------------------------------------------- /mdiff/api.py: -------------------------------------------------------------------------------- 1 | 2 | from .core import ( 3 | status, 4 | manage, 5 | update_verifiers, 6 | 7 | on_track, 8 | on_change, 9 | on_duplicate, 10 | 11 | is_duplicated, 12 | is_changed, 13 | get_time, 14 | read_uuid, 15 | read_address, 16 | read_verifier, 17 | read_fingerprint, 18 | 19 | Clean, 20 | Changed, 21 | Duplicated, 22 | Untracked, 23 | 24 | ATTR_ADDRESS, 25 | ATTR_VERIFIER, 26 | ATTR_FINGERPRINT, 27 | ) 28 | 29 | 30 | __all__ = ( 31 | "status", 32 | "manage", 33 | "update_verifiers", 34 | 35 | "on_track", 36 | "on_change", 37 | "on_duplicate", 38 | 39 | "is_duplicated", 40 | "is_changed", 41 | "get_time", 42 | "read_uuid", 43 | "read_address", 44 | "read_verifier", 45 | "read_fingerprint", 46 | 47 | "Clean", 48 | "Changed", 49 | "Duplicated", 50 | "Untracked", 51 | 52 | "ATTR_ADDRESS", 53 | "ATTR_VERIFIER", 54 | "ATTR_FINGERPRINT", 55 | ) 56 | -------------------------------------------------------------------------------- /mdiff/core.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import uuid 4 | import hashlib 5 | import maya.cmds as cmds 6 | 7 | try: 8 | import bson 9 | except ImportError: 10 | pass 11 | 12 | from datetime import datetime 13 | 14 | 15 | ATTR_ADDRESS = "address" 16 | ATTR_VERIFIER = "verifier" 17 | ATTR_FINGERPRINT = "fingerprint" 18 | 19 | Clean = 0 20 | Changed = 1 21 | Duplicated = 2 22 | Untracked = 3 23 | 24 | 25 | def _get_attr(node, attr): 26 | """Internal function for attribute getting 27 | """ 28 | try: 29 | return cmds.getAttr(node + "." + attr) 30 | except ValueError: 31 | return None 32 | 33 | 34 | def _add_attr(node, attr): 35 | """Internal function for attribute adding 36 | """ 37 | try: 38 | cmds.addAttr(node, longName=attr, dataType="string") 39 | except RuntimeError: 40 | # Attribute existed 41 | pass 42 | 43 | 44 | def _set_attr(node, attr, value): 45 | """Internal function for attribute setting 46 | """ 47 | try: 48 | cmds.setAttr(node + "." + attr, value, type="string") 49 | except RuntimeError: 50 | # Attribute not existed 51 | pass 52 | 53 | 54 | def read_address(node): 55 | """Read address value from node 56 | 57 | Arguments: 58 | node (str): Maya node name 59 | 60 | """ 61 | return _get_attr(node, ATTR_ADDRESS) 62 | 63 | 64 | def read_verifier(node): 65 | """Read verifier value from node 66 | 67 | Arguments: 68 | node (str): Maya node name 69 | 70 | """ 71 | return _get_attr(node, ATTR_VERIFIER) 72 | 73 | 74 | def read_fingerprint(node): 75 | """Read fingerprint value from node 76 | 77 | Arguments: 78 | node (str): Maya node name 79 | 80 | """ 81 | return _get_attr(node, ATTR_FINGERPRINT) 82 | 83 | 84 | def read_uuid(node): 85 | """Read uuid value from node 86 | 87 | Arguments: 88 | node (str): Maya node name 89 | 90 | """ 91 | muuid = cmds.ls(node, uuid=True) 92 | if not len(muuid): 93 | raise RuntimeError("Node not found.") 94 | elif len(muuid) > 1: 95 | raise RuntimeError("Found more then one node, use long name.") 96 | 97 | return muuid[0] 98 | 99 | 100 | def _generate_address(): 101 | """Internal function for generating time-embedded ID address 102 | 103 | Note: 104 | `bson.ObjectId` is about 1 time faster then `uuid.uuid1`. 105 | 106 | """ 107 | try: 108 | return str(bson.ObjectId()) # bson is faster 109 | except NameError: 110 | return str(uuid.uuid1())[:-18] # remove mac-addr 111 | 112 | 113 | def _generate_verifier(muuid, address): 114 | """Internal function for generating hash value from Maya UUID and address 115 | 116 | Arguments: 117 | muuid (str): Maya UUID string 118 | address (str): Previous generated address id from node 119 | 120 | Note: 121 | Faster then uuid5. 122 | 123 | """ 124 | hasher = hashlib.sha1() 125 | hasher.update(muuid + ":" + address) 126 | return hasher.hexdigest() 127 | 128 | 129 | def is_duplicated(node): 130 | """Is this node a duplicate ? 131 | 132 | Return True if the `node` is a duplicate, by comparing the `verifier` 133 | attribute. 134 | 135 | Arguments: 136 | node (str): Maya node name 137 | 138 | """ 139 | address = read_address(node) 140 | verifier = read_verifier(node) 141 | 142 | if not all((address, verifier)): 143 | # Node did not have the attributes for verification, 144 | # this is new node. 145 | return True 146 | else: 147 | return not verifier == _generate_verifier(read_uuid(node), address) 148 | 149 | 150 | def is_changed(node, fingerprint): 151 | """Has this node been modified ? 152 | 153 | Return True if the `node` has been changed, by comparing the `fingerprint` 154 | 155 | Arguments: 156 | node (str): Maya node name 157 | fingerprint (str): Maya node's hash value 158 | 159 | """ 160 | origin_fingerprint = read_fingerprint(node) 161 | 162 | if origin_fingerprint is None: 163 | # Node did not have the attributes for verification, 164 | # this is new node. 165 | return True 166 | else: 167 | return not fingerprint == origin_fingerprint 168 | 169 | 170 | def status(node, fingerprint): 171 | """Report `node` current state 172 | 173 | Return node state flag (int), in range 0 - 3: 174 | 0 == api.Clean 175 | 1 == api.Changed 176 | 2 == api.Duplicated 177 | 3 == api.Untracked 178 | 179 | Arguments: 180 | node (str): Maya node name 181 | fingerprint (str): Maya node's hash value 182 | 183 | Returns: 184 | (int): Node state flag 185 | 186 | """ 187 | return is_changed(node, fingerprint) | (is_duplicated(node) << 1) 188 | 189 | 190 | def on_track(node, fingerprint): 191 | """Update node's address and fingerprint 192 | 193 | MUST do this if `status` return flag `api.Untracked`. 194 | 195 | Arguments: 196 | node (str): Maya node name 197 | fingerprint (str): Maya node's hash value 198 | 199 | """ 200 | address = _generate_address() 201 | _add_attr(node, ATTR_ADDRESS) 202 | _set_attr(node, ATTR_ADDRESS, address) 203 | on_change(node, fingerprint) 204 | on_duplicate(node) 205 | 206 | 207 | def on_change(node, fingerprint): 208 | """Update node's fingerprint 209 | 210 | MUST do this if `status` return flag `api.Changed`. 211 | 212 | Arguments: 213 | node (str): Maya node name 214 | fingerprint (str): Maya node's hash value 215 | 216 | """ 217 | _add_attr(node, ATTR_FINGERPRINT) 218 | _set_attr(node, ATTR_FINGERPRINT, fingerprint) 219 | 220 | 221 | def on_duplicate(node, *args): 222 | """Update node's verifier 223 | 224 | MUST do this if `status` return flag `api.Duplicated`. 225 | 226 | Arguments: 227 | node (str): Maya node name 228 | fingerprint (str): Maya node's hash value 229 | 230 | """ 231 | address = read_address(node) 232 | if address is None: 233 | return 234 | 235 | verifier = _generate_verifier(read_uuid(node), address) 236 | _add_attr(node, ATTR_VERIFIER) 237 | _set_attr(node, ATTR_VERIFIER, verifier) 238 | 239 | 240 | __action_map = { 241 | Clean: (lambda n, f: None), 242 | Changed: on_change, 243 | Duplicated: on_duplicate, 244 | Untracked: on_track, 245 | } 246 | 247 | 248 | def manage(node, fingerprint, state): 249 | """Auto update node's identity attributes by input state 250 | 251 | Arguments: 252 | node (str): Maya node name 253 | fingerprint (str): Maya node's hash value 254 | state (int): State flag returned from `status` 255 | 256 | """ 257 | action = __action_map[state] 258 | action(node, fingerprint) 259 | 260 | 261 | def update_verifiers(nodes): 262 | """Update input nodes' verifier 263 | 264 | MUST do this on file-import. 265 | 266 | Arguments: 267 | nodes (list): A list of Maya node name 268 | 269 | """ 270 | for node in nodes: 271 | on_duplicate(node) 272 | 273 | 274 | def get_time(node): 275 | """Retrive datetime object from Maya node 276 | 277 | A little bonus gained from datetime embedded id. 278 | 279 | Arguments: 280 | node (str): Maya node name 281 | 282 | """ 283 | address = read_address(node) 284 | if address is None: 285 | return None 286 | 287 | if "-" in address: 288 | _ut = uuid.UUID(address + "-0000-000000000000").time 289 | time = datetime.fromtimestamp((_ut - 0x01b21dd213814000L) * 100 / 1e9) 290 | else: 291 | time = bson.ObjectId(address).generation_time 292 | 293 | return time 294 | -------------------------------------------------------------------------------- /mdiff/version.py: -------------------------------------------------------------------------------- 1 | 2 | VERSION_MAJOR = 0 3 | VERSION_MINOR = 1 4 | VERSION_PATCH = 0 5 | 6 | version_info = (VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH) 7 | version = '%i.%i.%i' % version_info 8 | __version__ = version 9 | 10 | __all__ = ['version', 'version_info', '__version__'] 11 | --------------------------------------------------------------------------------