1# ##### BEGIN GPL LICENSE BLOCK ##### 2# 3# This program is free software; you can redistribute it and/or 4# modify it under the terms of the GNU General Public License 5# as published by the Free Software Foundation; either version 2 6# of the License, or (at your option) any later version. 7# 8# This program is distributed in the hope that it will be useful, 9# but WITHOUT ANY WARRANTY; without even the implied warranty of 10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11# GNU General Public License for more details. 12# 13# You should have received a copy of the GNU General Public License 14# along with this program; if not, write to the Free Software Foundation, 15# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16# 17# ##### END GPL LICENSE BLOCK ##### 18 19# Copyright 2011, Ryan Inch 20 21from . import persistent_data 22 23import bpy 24 25from bpy.types import ( 26 PropertyGroup, 27 Operator, 28) 29 30from bpy.props import ( 31 StringProperty, 32 IntProperty, 33) 34 35move_triggered = False 36move_selection = [] 37move_active = None 38 39layer_collections = {} 40collection_tree = [] 41collection_state = {} 42qcd_collection_state = {} 43expanded = set() 44row_index = 0 45max_lvl = 0 46 47rto_history = { 48 "exclude": {}, 49 "exclude_all": {}, 50 "select": {}, 51 "select_all": {}, 52 "hide": {}, 53 "hide_all": {}, 54 "disable": {}, 55 "disable_all": {}, 56 "render": {}, 57 "render_all": {}, 58 "holdout": {}, 59 "holdout_all": {}, 60 "indirect": {}, 61 "indirect_all": {}, 62} 63 64qcd_history = {} 65 66expand_history = { 67 "target": "", 68 "history": [], 69 } 70 71phantom_history = { 72 "view_layer": "", 73 "initial_state": {}, 74 75 "exclude_history": {}, 76 "select_history": {}, 77 "hide_history": {}, 78 "disable_history": {}, 79 "render_history": {}, 80 "holdout_history": {}, 81 "indirect_history": {}, 82 83 "exclude_all_history": [], 84 "select_all_history": [], 85 "hide_all_history": [], 86 "disable_all_history": [], 87 "render_all_history": [], 88 "holdout_all_history": [], 89 "indirect_all_history": [], 90 } 91 92copy_buffer = { 93 "RTO": "", 94 "values": [] 95 } 96 97swap_buffer = { 98 "A": { 99 "RTO": "", 100 "values": [] 101 }, 102 "B": { 103 "RTO": "", 104 "values": [] 105 } 106 } 107 108 109def get_max_lvl(): 110 return max_lvl 111 112 113class QCDSlots(): 114 _slots = {} 115 overrides = set() 116 allow_update = True 117 118 def __init__(self): 119 self._slots = persistent_data.slots 120 self.overrides = persistent_data.overrides 121 122 def __iter__(self): 123 return self._slots.items().__iter__() 124 125 def __repr__(self): 126 return self._slots.__repr__() 127 128 def contains(self, *, idx=None, name=None): 129 if idx: 130 return idx in self._slots.keys() 131 132 if name: 133 return name in self._slots.values() 134 135 raise 136 137 def object_in_slots(self, obj): 138 for collection in obj.users_collection: 139 if self.contains(name=collection.name): 140 return True 141 142 return False 143 144 def get_data_for_blend(self): 145 return f"{self._slots.__repr__()}\n{self.overrides.__repr__()}" 146 147 def load_blend_data(self, data): 148 decoupled_data = data.split("\n") 149 blend_slots = eval(decoupled_data[0]) 150 blend_overrides = eval(decoupled_data[1]) 151 152 self._slots.clear() 153 self.overrides.clear() 154 155 for key, value in blend_slots.items(): 156 self._slots[key] = value 157 158 for key in blend_overrides: 159 self.overrides.add(key) 160 161 def length(self): 162 return len(self._slots) 163 164 def get_idx(self, name, r_value=None): 165 for idx, slot_name in self._slots.items(): 166 if slot_name == name: 167 return idx 168 169 return r_value 170 171 def get_name(self, idx, r_value=None): 172 if idx in self._slots: 173 return self._slots[idx] 174 175 return r_value 176 177 def add_slot(self, idx, name): 178 self._slots[idx] = name 179 180 if name in self.overrides: 181 self.overrides.remove(name) 182 183 def update_slot(self, idx, name): 184 self.add_slot(idx, name) 185 186 def del_slot(self, *, idx=None, name=None): 187 if idx and not name: 188 del self._slots[idx] 189 return 190 191 if name and not idx: 192 slot_idx = self.get_idx(name) 193 del self._slots[slot_idx] 194 return 195 196 raise 197 198 def add_override(self, name): 199 qcd_slots.del_slot(name=name) 200 qcd_slots.overrides.add(name) 201 202 def clear_slots(self): 203 self._slots.clear() 204 205 def update_qcd(self): 206 for idx, name in list(self._slots.items()): 207 if not layer_collections.get(name, None): 208 qcd_slots.del_slot(name=name) 209 210 def auto_numerate(self): 211 if self.length() < 20: 212 laycol = bpy.context.view_layer.layer_collection 213 214 laycol_iter_list = list(laycol.children) 215 while laycol_iter_list: 216 layer_collection = laycol_iter_list.pop(0) 217 laycol_iter_list.extend(list(layer_collection.children)) 218 219 if layer_collection.name in qcd_slots.overrides: 220 continue 221 222 for x in range(20): 223 if (not self.contains(idx=str(x+1)) and 224 not self.contains(name=layer_collection.name)): 225 self.add_slot(str(x+1), layer_collection.name) 226 227 228 if self.length() > 20: 229 break 230 231 def renumerate(self, *, beginning=False, depth_first=False, constrain=False): 232 if beginning: 233 self.clear_slots() 234 self.overrides.clear() 235 236 starting_laycol_name = self.get_name("1") 237 238 if not starting_laycol_name: 239 laycol = bpy.context.view_layer.layer_collection 240 starting_laycol_name = laycol.children[0].name 241 242 self.clear_slots() 243 self.overrides.clear() 244 245 if depth_first: 246 parent = layer_collections[starting_laycol_name]["parent"] 247 x = 1 248 249 for laycol in layer_collections.values(): 250 if self.length() == 0 and starting_laycol_name != laycol["name"]: 251 continue 252 253 if constrain: 254 if self.length(): 255 if laycol["parent"]["name"] == parent["name"]: 256 break 257 258 self.add_slot(f"{x}", laycol["name"]) 259 260 x += 1 261 262 if self.length() > 20: 263 break 264 265 else: 266 laycol = layer_collections[starting_laycol_name]["parent"]["ptr"] 267 268 laycol_iter_list = [] 269 for laycol in laycol.children: 270 if laycol.name == starting_laycol_name: 271 laycol_iter_list.append(laycol) 272 273 elif not constrain and laycol_iter_list: 274 laycol_iter_list.append(laycol) 275 276 x = 1 277 while laycol_iter_list: 278 layer_collection = laycol_iter_list.pop(0) 279 280 self.add_slot(f"{x}", layer_collection.name) 281 282 laycol_iter_list.extend(list(layer_collection.children)) 283 284 x += 1 285 286 if self.length() > 20: 287 break 288 289 290 for laycol in layer_collections.values(): 291 if not self.contains(name=laycol["name"]): 292 self.overrides.add(laycol["name"]) 293 294qcd_slots = QCDSlots() 295 296 297def update_col_name(self, context): 298 global layer_collections 299 global qcd_slots 300 global rto_history 301 global expand_history 302 303 if self.name != self.last_name: 304 if self.name == '': 305 self.name = self.last_name 306 return 307 308 # if statement prevents update on list creation 309 if self.last_name != '': 310 view_layer_name = context.view_layer.name 311 312 # update collection name 313 layer_collections[self.last_name]["ptr"].collection.name = self.name 314 315 # update expanded 316 orig_expanded = {x for x in expanded} 317 318 if self.last_name in orig_expanded: 319 expanded.remove(self.last_name) 320 expanded.add(self.name) 321 322 # update qcd_slot 323 idx = qcd_slots.get_idx(self.last_name) 324 if idx: 325 qcd_slots.update_slot(idx, self.name) 326 327 # update qcd_overrides 328 if self.last_name in qcd_slots.overrides: 329 qcd_slots.overrides.remove(self.last_name) 330 qcd_slots.overrides.add(self.name) 331 332 # update history 333 rtos = [ 334 "exclude", 335 "select", 336 "hide", 337 "disable", 338 "render", 339 "holdout", 340 "indirect", 341 ] 342 343 orig_targets = { 344 rto: rto_history[rto][view_layer_name]["target"] 345 for rto in rtos 346 if rto_history[rto].get(view_layer_name) 347 } 348 349 for rto in rtos: 350 history = rto_history[rto].get(view_layer_name) 351 352 if history and orig_targets[rto] == self.last_name: 353 history["target"] = self.name 354 355 # update expand history 356 orig_expand_target = expand_history["target"] 357 orig_expand_history = [x for x in expand_history["history"]] 358 359 if orig_expand_target == self.last_name: 360 expand_history["target"] = self.name 361 362 for x, name in enumerate(orig_expand_history): 363 if name == self.last_name: 364 expand_history["history"][x] = self.name 365 366 # update names in expanded, qcd slots, and rto_history for any other 367 # collection names that changed as a result of this name change 368 cm_list_collection = context.scene.collection_manager.cm_list_collection 369 count = 0 370 laycol_iter_list = list(context.view_layer.layer_collection.children) 371 372 while laycol_iter_list: 373 layer_collection = laycol_iter_list[0] 374 cm_list_item = cm_list_collection[count] 375 376 if cm_list_item.name != layer_collection.name: 377 # update expanded 378 if cm_list_item.last_name in orig_expanded: 379 if not cm_list_item.last_name in layer_collections: 380 expanded.remove(cm_list_item.name) 381 382 expanded.add(layer_collection.name) 383 384 # update qcd_slot 385 idx = cm_list_item.qcd_slot_idx 386 if idx: 387 qcd_slots.update_slot(idx, layer_collection.name) 388 389 # update qcd_overrides 390 if cm_list_item.name in qcd_slots.overrides: 391 if not cm_list_item.name in layer_collections: 392 qcd_slots.overrides.remove(cm_list_item.name) 393 394 qcd_slots.overrides.add(layer_collection.name) 395 396 # update history 397 for rto in rtos: 398 history = rto_history[rto].get(view_layer_name) 399 400 if history and orig_targets[rto] == cm_list_item.last_name: 401 history["target"] = layer_collection.name 402 403 # update expand history 404 if orig_expand_target == cm_list_item.last_name: 405 expand_history["target"] = layer_collection.name 406 407 for x, name in enumerate(orig_expand_history): 408 if name == cm_list_item.last_name: 409 expand_history["history"][x] = layer_collection.name 410 411 if layer_collection.children: 412 laycol_iter_list[0:0] = list(layer_collection.children) 413 414 415 laycol_iter_list.remove(layer_collection) 416 count += 1 417 418 419 update_property_group(context) 420 421 422 self.last_name = self.name 423 424 425def update_qcd_slot(self, context): 426 global qcd_slots 427 428 if not qcd_slots.allow_update: 429 return 430 431 update_needed = False 432 433 try: 434 int(self.qcd_slot_idx) 435 436 except ValueError: 437 if self.qcd_slot_idx == "": 438 qcd_slots.add_override(self.name) 439 440 if qcd_slots.contains(name=self.name): 441 qcd_slots.allow_update = False 442 self.qcd_slot_idx = qcd_slots.get_idx(self.name) 443 qcd_slots.allow_update = True 444 445 if self.name in qcd_slots.overrides: 446 qcd_slots.allow_update = False 447 self.qcd_slot_idx = "" 448 qcd_slots.allow_update = True 449 450 return 451 452 if qcd_slots.contains(name=self.name): 453 qcd_slots.del_slot(name=self.name) 454 update_needed = True 455 456 if qcd_slots.contains(idx=self.qcd_slot_idx): 457 qcd_slots.add_override(qcd_slots.get_name(self.qcd_slot_idx)) 458 update_needed = True 459 460 if int(self.qcd_slot_idx) > 20: 461 self.qcd_slot_idx = "20" 462 463 if int(self.qcd_slot_idx) < 1: 464 self.qcd_slot_idx = "1" 465 466 qcd_slots.add_slot(self.qcd_slot_idx, self.name) 467 468 if update_needed: 469 update_property_group(context) 470 471 472class CMListCollection(PropertyGroup): 473 name: StringProperty(update=update_col_name) 474 last_name: StringProperty() 475 qcd_slot_idx: StringProperty(name="QCD Slot", update=update_qcd_slot) 476 477 478def update_collection_tree(context): 479 global max_lvl 480 global row_index 481 global collection_tree 482 global layer_collections 483 global qcd_slots 484 485 collection_tree.clear() 486 layer_collections.clear() 487 488 max_lvl = 0 489 row_index = 0 490 layer_collection = context.view_layer.layer_collection 491 init_laycol_list = layer_collection.children 492 493 master_laycol = {"id": 0, 494 "name": layer_collection.name, 495 "lvl": -1, 496 "row_index": -1, 497 "visible": True, 498 "has_children": True, 499 "expanded": True, 500 "parent": None, 501 "children": [], 502 "ptr": layer_collection 503 } 504 505 get_all_collections(context, init_laycol_list, master_laycol, master_laycol["children"], visible=True) 506 507 for laycol in master_laycol["children"]: 508 collection_tree.append(laycol) 509 510 qcd_slots.update_qcd() 511 512 qcd_slots.auto_numerate() 513 514 515def get_all_collections(context, collections, parent, tree, level=0, visible=False): 516 global row_index 517 global max_lvl 518 519 if level > max_lvl: 520 max_lvl = level 521 522 for item in collections: 523 laycol = {"id": len(layer_collections) +1, 524 "name": item.name, 525 "lvl": level, 526 "row_index": row_index, 527 "visible": visible, 528 "has_children": False, 529 "expanded": False, 530 "parent": parent, 531 "children": [], 532 "ptr": item 533 } 534 535 row_index += 1 536 537 layer_collections[item.name] = laycol 538 tree.append(laycol) 539 540 if len(item.children) > 0: 541 laycol["has_children"] = True 542 543 if item.name in expanded and laycol["visible"]: 544 laycol["expanded"] = True 545 get_all_collections(context, item.children, laycol, laycol["children"], level+1, visible=True) 546 547 else: 548 get_all_collections(context, item.children, laycol, laycol["children"], level+1) 549 550 551def update_property_group(context): 552 global collection_tree 553 global qcd_slots 554 555 qcd_slots.allow_update = False 556 557 update_collection_tree(context) 558 context.scene.collection_manager.cm_list_collection.clear() 559 create_property_group(context, collection_tree) 560 561 qcd_slots.allow_update = True 562 563 564def create_property_group(context, tree): 565 global in_filter 566 global qcd_slots 567 568 cm = context.scene.collection_manager 569 570 for laycol in tree: 571 new_cm_listitem = cm.cm_list_collection.add() 572 new_cm_listitem.name = laycol["name"] 573 new_cm_listitem.qcd_slot_idx = qcd_slots.get_idx(laycol["name"], "") 574 575 if laycol["has_children"]: 576 create_property_group(context, laycol["children"]) 577 578 579def get_modifiers(event): 580 modifiers = [] 581 582 if event.alt: 583 modifiers.append("alt") 584 585 if event.ctrl: 586 modifiers.append("ctrl") 587 588 if event.oskey: 589 modifiers.append("oskey") 590 591 if event.shift: 592 modifiers.append("shift") 593 594 return set(modifiers) 595 596 597def generate_state(*, qcd=False): 598 global layer_collections 599 global qcd_slots 600 601 state = { 602 "name": [], 603 "exclude": [], 604 "select": [], 605 "hide": [], 606 "disable": [], 607 "render": [], 608 "holdout": [], 609 "indirect": [], 610 } 611 612 for name, laycol in layer_collections.items(): 613 state["name"].append(name) 614 state["exclude"].append(laycol["ptr"].exclude) 615 state["select"].append(laycol["ptr"].collection.hide_select) 616 state["hide"].append(laycol["ptr"].hide_viewport) 617 state["disable"].append(laycol["ptr"].collection.hide_viewport) 618 state["render"].append(laycol["ptr"].collection.hide_render) 619 state["holdout"].append(laycol["ptr"].holdout) 620 state["indirect"].append(laycol["ptr"].indirect_only) 621 622 if qcd: 623 state["qcd"] = dict(qcd_slots) 624 625 return state 626 627 628def get_move_selection(*, names_only=False): 629 global move_selection 630 631 if not move_selection: 632 move_selection = {obj.name for obj in bpy.context.selected_objects} 633 634 if names_only: 635 return move_selection 636 637 else: 638 if len(move_selection) <= 5: 639 return {bpy.data.objects[name] for name in move_selection} 640 641 else: 642 return {obj for obj in bpy.data.objects if obj.name in move_selection} 643 644 645def get_move_active(): 646 global move_active 647 global move_selection 648 649 if not move_active: 650 move_active = getattr(bpy.context.view_layer.objects.active, "name", None) 651 652 if move_active not in get_move_selection(names_only=True): 653 move_active = None 654 655 return bpy.data.objects[move_active] if move_active else None 656 657 658def update_qcd_header(): 659 cm = bpy.context.scene.collection_manager 660 cm.update_header.clear() 661 new_update_header = cm.update_header.add() 662 new_update_header.name = "updated" 663 664 665class CMSendReport(Operator): 666 bl_label = "Send Report" 667 bl_idname = "view3d.cm_send_report" 668 669 message: StringProperty() 670 671 def draw(self, context): 672 layout = self.layout 673 col = layout.column(align=True) 674 675 first = True 676 string = "" 677 678 for num, char in enumerate(self.message): 679 if char == "\n": 680 if first: 681 col.row(align=True).label(text=string, icon='ERROR') 682 first = False 683 else: 684 col.row(align=True).label(text=string, icon='BLANK1') 685 686 string = "" 687 continue 688 689 string = string + char 690 691 if first: 692 col.row(align=True).label(text=string, icon='ERROR') 693 else: 694 col.row(align=True).label(text=string, icon='BLANK1') 695 696 def invoke(self, context, event): 697 wm = context.window_manager 698 699 max_len = 0 700 length = 0 701 702 for char in self.message: 703 if char == "\n": 704 if length > max_len: 705 max_len = length 706 length = 0 707 else: 708 length += 1 709 710 if length > max_len: 711 max_len = length 712 713 return wm.invoke_popup(self, width=(30 + (max_len*5.5))) 714 715 def execute(self, context): 716 self.report({'INFO'}, self.message) 717 print(self.message) 718 return {'FINISHED'} 719 720def send_report(message): 721 def report(): 722 window = bpy.context.window_manager.windows[0] 723 ctx = {'window': window, 'screen': window.screen, } 724 bpy.ops.view3d.cm_send_report(ctx, 'INVOKE_DEFAULT', message=message) 725 726 bpy.app.timers.register(report) 727