1 # SPDX-License-Identifier: GPL-2.0-or-later
3 # Copyright 2011, Ryan Inch
5 from . import persistent_data
9 from bpy
.types
import (
14 from bpy
.props
import (
19 move_triggered
= False
23 layer_collections
= {}
26 qcd_collection_state
= {}
59 "exclude_history": {},
62 "disable_history": {},
64 "holdout_history": {},
65 "indirect_history": {},
67 "exclude_all_history": [],
68 "select_all_history": [],
69 "hide_all_history": [],
70 "disable_all_history": [],
71 "render_all_history": [],
72 "holdout_all_history": [],
73 "indirect_all_history": [],
99 self
._slots
= persistent_data
.slots
100 self
.overrides
= persistent_data
.overrides
103 return self
._slots
.items().__iter
__()
106 return self
._slots
.__repr
__()
108 def contains(self
, *, idx
=None, name
=None):
110 return idx
in self
._slots
.keys()
113 return name
in self
._slots
.values()
117 def object_in_slots(self
, obj
):
118 for collection
in obj
.users_collection
:
119 if self
.contains(name
=collection
.name
):
124 def get_data_for_blend(self
):
125 return f
"{self._slots.__repr__()}\n{self.overrides.__repr__()}"
127 def load_blend_data(self
, data
):
128 decoupled_data
= data
.split("\n")
129 blend_slots
= eval(decoupled_data
[0])
130 blend_overrides
= eval(decoupled_data
[1])
133 self
.overrides
.clear()
135 for key
, value
in blend_slots
.items():
136 self
._slots
[key
] = value
138 for key
in blend_overrides
:
139 self
.overrides
.add(key
)
142 return len(self
._slots
)
144 def get_idx(self
, name
, r_value
=None):
145 for idx
, slot_name
in self
._slots
.items():
146 if slot_name
== name
:
151 def get_name(self
, idx
, r_value
=None):
152 if idx
in self
._slots
:
153 return self
._slots
[idx
]
157 def add_slot(self
, idx
, name
):
158 self
._slots
[idx
] = name
160 if name
in self
.overrides
:
161 self
.overrides
.remove(name
)
163 def update_slot(self
, idx
, name
):
164 self
.add_slot(idx
, name
)
166 def del_slot(self
, *, idx
=None, name
=None):
172 slot_idx
= self
.get_idx(name
)
173 del self
._slots
[slot_idx
]
178 def add_override(self
, name
):
179 qcd_slots
.del_slot(name
=name
)
180 qcd_slots
.overrides
.add(name
)
182 def clear_slots(self
):
185 def update_qcd(self
):
186 for idx
, name
in list(self
._slots
.items()):
187 if not layer_collections
.get(name
, None):
188 qcd_slots
.del_slot(name
=name
)
190 def auto_numerate(self
):
191 if self
.length() < 20:
192 laycol
= bpy
.context
.view_layer
.layer_collection
194 laycol_iter_list
= list(laycol
.children
)
195 while laycol_iter_list
:
196 layer_collection
= laycol_iter_list
.pop(0)
197 laycol_iter_list
.extend(list(layer_collection
.children
))
199 if layer_collection
.name
in qcd_slots
.overrides
:
203 if (not self
.contains(idx
=str(x
+1)) and
204 not self
.contains(name
=layer_collection
.name
)):
205 self
.add_slot(str(x
+1), layer_collection
.name
)
208 if self
.length() > 20:
211 def renumerate(self
, *, beginning
=False, depth_first
=False, constrain
=False):
214 self
.overrides
.clear()
216 starting_laycol_name
= self
.get_name("1")
218 if not starting_laycol_name
:
219 laycol
= bpy
.context
.view_layer
.layer_collection
220 starting_laycol_name
= laycol
.children
[0].name
223 self
.overrides
.clear()
226 parent
= layer_collections
[starting_laycol_name
]["parent"]
229 for laycol
in layer_collections
.values():
230 if self
.length() == 0 and starting_laycol_name
!= laycol
["name"]:
235 if laycol
["parent"]["name"] == parent
["name"]:
238 self
.add_slot(f
"{x}", laycol
["name"])
242 if self
.length() == 20:
246 laycol
= layer_collections
[starting_laycol_name
]["parent"]["ptr"]
248 laycol_iter_list
= []
249 for laycol
in laycol
.children
:
250 if laycol
.name
== starting_laycol_name
:
251 laycol_iter_list
.append(laycol
)
253 elif not constrain
and laycol_iter_list
:
254 laycol_iter_list
.append(laycol
)
257 while laycol_iter_list
:
258 layer_collection
= laycol_iter_list
.pop(0)
260 self
.add_slot(f
"{x}", layer_collection
.name
)
262 laycol_iter_list
.extend(list(layer_collection
.children
))
266 if self
.length() == 20:
270 for laycol
in layer_collections
.values():
271 if not self
.contains(name
=laycol
["name"]):
272 self
.overrides
.add(laycol
["name"])
274 qcd_slots
= QCDSlots()
277 def update_col_name(self
, context
):
278 global layer_collections
281 global expand_history
283 if self
.name
!= self
.last_name
:
285 self
.name
= self
.last_name
288 # if statement prevents update on list creation
289 if self
.last_name
!= '':
290 view_layer_name
= context
.view_layer
.name
292 # update collection name
293 layer_collections
[self
.last_name
]["ptr"].collection
.name
= self
.name
296 orig_expanded
= {x
for x
in expanded
}
298 if self
.last_name
in orig_expanded
:
299 expanded
.remove(self
.last_name
)
300 expanded
.add(self
.name
)
303 idx
= qcd_slots
.get_idx(self
.last_name
)
305 qcd_slots
.update_slot(idx
, self
.name
)
307 # update qcd_overrides
308 if self
.last_name
in qcd_slots
.overrides
:
309 qcd_slots
.overrides
.remove(self
.last_name
)
310 qcd_slots
.overrides
.add(self
.name
)
324 rto
: rto_history
[rto
][view_layer_name
]["target"]
326 if rto_history
[rto
].get(view_layer_name
)
330 history
= rto_history
[rto
].get(view_layer_name
)
332 if history
and orig_targets
[rto
] == self
.last_name
:
333 history
["target"] = self
.name
335 # update expand history
336 orig_expand_target
= expand_history
["target"]
337 orig_expand_history
= [x
for x
in expand_history
["history"]]
339 if orig_expand_target
== self
.last_name
:
340 expand_history
["target"] = self
.name
342 for x
, name
in enumerate(orig_expand_history
):
343 if name
== self
.last_name
:
344 expand_history
["history"][x
] = self
.name
346 # update names in expanded, qcd slots, and rto_history for any other
347 # collection names that changed as a result of this name change
348 cm_list_collection
= context
.scene
.collection_manager
.cm_list_collection
350 laycol_iter_list
= list(context
.view_layer
.layer_collection
.children
)
352 while laycol_iter_list
:
353 layer_collection
= laycol_iter_list
[0]
354 cm_list_item
= cm_list_collection
[count
]
356 if cm_list_item
.name
!= layer_collection
.name
:
358 if cm_list_item
.last_name
in orig_expanded
:
359 if not cm_list_item
.last_name
in layer_collections
:
360 expanded
.remove(cm_list_item
.name
)
362 expanded
.add(layer_collection
.name
)
365 idx
= cm_list_item
.qcd_slot_idx
367 qcd_slots
.update_slot(idx
, layer_collection
.name
)
369 # update qcd_overrides
370 if cm_list_item
.name
in qcd_slots
.overrides
:
371 if not cm_list_item
.name
in layer_collections
:
372 qcd_slots
.overrides
.remove(cm_list_item
.name
)
374 qcd_slots
.overrides
.add(layer_collection
.name
)
378 history
= rto_history
[rto
].get(view_layer_name
)
380 if history
and orig_targets
[rto
] == cm_list_item
.last_name
:
381 history
["target"] = layer_collection
.name
383 # update expand history
384 if orig_expand_target
== cm_list_item
.last_name
:
385 expand_history
["target"] = layer_collection
.name
387 for x
, name
in enumerate(orig_expand_history
):
388 if name
== cm_list_item
.last_name
:
389 expand_history
["history"][x
] = layer_collection
.name
391 if layer_collection
.children
:
392 laycol_iter_list
[0:0] = list(layer_collection
.children
)
395 laycol_iter_list
.remove(layer_collection
)
399 update_property_group(context
)
402 self
.last_name
= self
.name
405 def update_qcd_slot(self
, context
):
408 if not qcd_slots
.allow_update
:
411 update_needed
= False
414 int(self
.qcd_slot_idx
)
417 if self
.qcd_slot_idx
== "":
418 qcd_slots
.add_override(self
.name
)
420 if qcd_slots
.contains(name
=self
.name
):
421 qcd_slots
.allow_update
= False
422 self
.qcd_slot_idx
= qcd_slots
.get_idx(self
.name
)
423 qcd_slots
.allow_update
= True
425 if self
.name
in qcd_slots
.overrides
:
426 qcd_slots
.allow_update
= False
427 self
.qcd_slot_idx
= ""
428 qcd_slots
.allow_update
= True
432 if qcd_slots
.contains(name
=self
.name
):
433 qcd_slots
.del_slot(name
=self
.name
)
436 if qcd_slots
.contains(idx
=self
.qcd_slot_idx
):
437 qcd_slots
.add_override(qcd_slots
.get_name(self
.qcd_slot_idx
))
440 if int(self
.qcd_slot_idx
) > 20:
441 self
.qcd_slot_idx
= "20"
443 if int(self
.qcd_slot_idx
) < 1:
444 self
.qcd_slot_idx
= "1"
446 qcd_slots
.add_slot(self
.qcd_slot_idx
, self
.name
)
449 update_property_group(context
)
452 class CMListCollection(PropertyGroup
):
453 name
: StringProperty(update
=update_col_name
)
454 last_name
: StringProperty()
455 qcd_slot_idx
: StringProperty(name
="QCD Slot", update
=update_qcd_slot
)
458 def update_collection_tree(context
):
461 global collection_tree
462 global layer_collections
465 collection_tree
.clear()
466 layer_collections
.clear()
470 layer_collection
= context
.view_layer
.layer_collection
471 init_laycol_list
= layer_collection
.children
473 master_laycol
= {"id": 0,
474 "name": layer_collection
.name
,
478 "has_children": True,
482 "ptr": layer_collection
485 get_all_collections(context
, init_laycol_list
, master_laycol
, master_laycol
["children"], visible
=True)
487 for laycol
in master_laycol
["children"]:
488 collection_tree
.append(laycol
)
490 qcd_slots
.update_qcd()
492 qcd_slots
.auto_numerate()
495 def get_all_collections(context
, collections
, parent
, tree
, level
=0, visible
=False):
502 for item
in collections
:
503 laycol
= {"id": len(layer_collections
) +1,
506 "row_index": row_index
,
508 "has_children": False,
517 layer_collections
[item
.name
] = laycol
520 if len(item
.children
) > 0:
521 laycol
["has_children"] = True
523 if item
.name
in expanded
and laycol
["visible"]:
524 laycol
["expanded"] = True
525 get_all_collections(context
, item
.children
, laycol
, laycol
["children"], level
+1, visible
=True)
528 get_all_collections(context
, item
.children
, laycol
, laycol
["children"], level
+1)
531 def update_property_group(context
):
532 global collection_tree
535 qcd_slots
.allow_update
= False
537 update_collection_tree(context
)
538 context
.scene
.collection_manager
.cm_list_collection
.clear()
539 create_property_group(context
, collection_tree
)
541 qcd_slots
.allow_update
= True
544 def create_property_group(context
, tree
):
548 cm
= context
.scene
.collection_manager
551 new_cm_listitem
= cm
.cm_list_collection
.add()
552 new_cm_listitem
.name
= laycol
["name"]
553 new_cm_listitem
.qcd_slot_idx
= qcd_slots
.get_idx(laycol
["name"], "")
555 if laycol
["has_children"]:
556 create_property_group(context
, laycol
["children"])
559 def get_modifiers(event
):
563 modifiers
.append("alt")
566 modifiers
.append("ctrl")
569 modifiers
.append("oskey")
572 modifiers
.append("shift")
574 return set(modifiers
)
577 def generate_state(*, qcd
=False):
578 global layer_collections
592 for name
, laycol
in layer_collections
.items():
593 state
["name"].append(name
)
594 state
["exclude"].append(laycol
["ptr"].exclude
)
595 state
["select"].append(laycol
["ptr"].collection
.hide_select
)
596 state
["hide"].append(laycol
["ptr"].hide_viewport
)
597 state
["disable"].append(laycol
["ptr"].collection
.hide_viewport
)
598 state
["render"].append(laycol
["ptr"].collection
.hide_render
)
599 state
["holdout"].append(laycol
["ptr"].holdout
)
600 state
["indirect"].append(laycol
["ptr"].indirect_only
)
603 state
["qcd"] = dict(qcd_slots
)
608 def check_state(context
, *, cm_popup
=False, phantom_mode
=False, qcd
=False):
609 view_layer
= context
.view_layer
611 # check if expanded & history/buffer state still correct
612 if cm_popup
and collection_state
:
613 new_state
= generate_state()
615 if new_state
["name"] != collection_state
["name"]:
616 copy_buffer
["RTO"] = ""
617 copy_buffer
["values"].clear()
619 swap_buffer
["A"]["RTO"] = ""
620 swap_buffer
["A"]["values"].clear()
621 swap_buffer
["B"]["RTO"] = ""
622 swap_buffer
["B"]["values"].clear()
624 for name
in list(expanded
):
625 laycol
= layer_collections
.get(name
)
626 if not laycol
or not laycol
["has_children"]:
627 expanded
.remove(name
)
629 for name
in list(expand_history
["history"]):
630 laycol
= layer_collections
.get(name
)
631 if not laycol
or not laycol
["has_children"]:
632 expand_history
["history"].remove(name
)
634 for rto
, history
in rto_history
.items():
635 if view_layer
.name
in history
:
636 del history
[view_layer
.name
]
640 for rto
in ["exclude", "select", "hide", "disable", "render", "holdout", "indirect"]:
641 if new_state
[rto
] != collection_state
[rto
]:
642 if view_layer
.name
in rto_history
[rto
]:
643 del rto_history
[rto
][view_layer
.name
]
645 if view_layer
.name
in rto_history
[rto
+"_all"]:
646 del rto_history
[rto
+"_all"][view_layer
.name
]
650 cm
= context
.scene
.collection_manager
652 # check if in phantom mode and if it's still viable
653 if cm
.in_phantom_mode
:
654 if layer_collections
.keys() != phantom_history
["initial_state"].keys():
655 cm
.in_phantom_mode
= False
657 if view_layer
.name
!= phantom_history
["view_layer"]:
658 cm
.in_phantom_mode
= False
660 if not cm
.in_phantom_mode
:
661 for key
, value
in phantom_history
.items():
664 except AttributeError:
665 if key
== "view_layer":
666 phantom_history
["view_layer"] = ""
669 if qcd
and qcd_collection_state
:
670 from .qcd_operators
import QCDAllBase
671 new_state
= generate_state(qcd
=True)
673 if (new_state
["name"] != qcd_collection_state
["name"]
674 or new_state
["exclude"] != qcd_collection_state
["exclude"]
675 or new_state
["qcd"] != qcd_collection_state
["qcd"]):
676 if view_layer
.name
in qcd_history
:
677 del qcd_history
[view_layer
.name
]
678 qcd_collection_state
.clear()
682 def get_move_selection(*, names_only
=False):
683 global move_selection
685 if not move_selection
:
686 move_selection
= {obj
.name
for obj
in bpy
.context
.selected_objects
}
689 return move_selection
692 if len(move_selection
) <= 5:
693 return {bpy
.data
.objects
[name
] for name
in move_selection
}
696 return {obj
for obj
in bpy
.data
.objects
if obj
.name
in move_selection
}
699 def get_move_active(*, always
=False):
701 global move_selection
704 move_active
= getattr(bpy
.context
.view_layer
.objects
.active
, "name", None)
706 if not always
and move_active
not in get_move_selection(names_only
=True):
709 return bpy
.data
.objects
[move_active
] if move_active
else None
712 def update_qcd_header():
713 cm
= bpy
.context
.scene
.collection_manager
714 cm
.update_header
.clear()
715 new_update_header
= cm
.update_header
.add()
716 new_update_header
.name
= "updated"
719 class CMSendReport(Operator
):
720 bl_label
= "Send Report"
721 bl_idname
= "view3d.cm_send_report"
723 message
: StringProperty()
725 def draw(self
, context
):
727 col
= layout
.column(align
=True)
732 for num
, char
in enumerate(self
.message
):
735 col
.row(align
=True).label(text
=string
, icon
='ERROR')
738 col
.row(align
=True).label(text
=string
, icon
='BLANK1')
743 string
= string
+ char
746 col
.row(align
=True).label(text
=string
, icon
='ERROR')
748 col
.row(align
=True).label(text
=string
, icon
='BLANK1')
750 def invoke(self
, context
, event
):
751 wm
= context
.window_manager
756 for char
in self
.message
:
767 return wm
.invoke_popup(self
, width
=(30 + (max_len
*5.5)))
769 def execute(self
, context
):
770 self
.report({'INFO'}, self
.message
)
774 def send_report(message
):
776 window
= bpy
.context
.window_manager
.windows
[0]
777 ctx
= {'window': window
, 'screen': window
.screen
, }
778 bpy
.ops
.view3d
.cm_send_report(ctx
, 'INVOKE_DEFAULT', message
=message
)
780 bpy
.app
.timers
.register(report
)
783 class CMUISeparatorButton(Operator
):
784 bl_label
= "UI Separator Button"
785 bl_idname
= "view3d.cm_ui_separator_button"
787 def execute(self
, context
):
790 def add_vertical_separator_line(row
):
791 # add buffer before to account for scaling
792 separator
= row
.row()
793 separator
.scale_x
= 0.1
797 separator
= row
.row()
798 separator
.scale_x
= 0.2
799 separator
.enabled
= False
800 separator
.operator("view3d.cm_ui_separator_button",
804 # add buffer after to account for scaling
805 separator
= row
.row()
806 separator
.scale_x
= 0.1