File headers: use SPDX license identifiers
[blender-addons.git] / object_collection_manager / operator_utils.py
blob51b4385d551b24f368ceb0003f71a2f39c8cefbf
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 # Copyright 2011, Ryan Inch
4 import bpy
6 # For VARS
7 from . import internals
9 # For FUNCTIONS
10 from .internals import (
11 update_property_group,
12 get_move_selection,
13 get_move_active,
16 mode_converter = {
17 'EDIT_MESH': 'EDIT',
18 'EDIT_CURVE': 'EDIT',
19 'EDIT_SURFACE': 'EDIT',
20 'EDIT_TEXT': 'EDIT',
21 'EDIT_ARMATURE': 'EDIT',
22 'EDIT_METABALL': 'EDIT',
23 'EDIT_LATTICE': 'EDIT',
24 'POSE': 'POSE',
25 'SCULPT': 'SCULPT',
26 'PAINT_WEIGHT': 'WEIGHT_PAINT',
27 'PAINT_VERTEX': 'VERTEX_PAINT',
28 'PAINT_TEXTURE': 'TEXTURE_PAINT',
29 'PARTICLE': 'PARTICLE_EDIT',
30 'OBJECT': 'OBJECT',
31 'PAINT_GPENCIL': 'PAINT_GPENCIL',
32 'EDIT_GPENCIL': 'EDIT_GPENCIL',
33 'SCULPT_GPENCIL': 'SCULPT_GPENCIL',
34 'WEIGHT_GPENCIL': 'WEIGHT_GPENCIL',
35 'VERTEX_GPENCIL': 'VERTEX_GPENCIL',
39 rto_path = {
40 "exclude": "exclude",
41 "select": "collection.hide_select",
42 "hide": "hide_viewport",
43 "disable": "collection.hide_viewport",
44 "render": "collection.hide_render",
45 "holdout": "holdout",
46 "indirect": "indirect_only",
49 set_off_on = {
50 "exclude": {
51 "off": True,
52 "on": False
54 "select": {
55 "off": True,
56 "on": False
58 "hide": {
59 "off": True,
60 "on": False
62 "disable": {
63 "off": True,
64 "on": False
66 "render": {
67 "off": True,
68 "on": False
70 "holdout": {
71 "off": False,
72 "on": True
74 "indirect": {
75 "off": False,
76 "on": True
80 get_off_on = {
81 False: {
82 "exclude": "on",
83 "select": "on",
84 "hide": "on",
85 "disable": "on",
86 "render": "on",
87 "holdout": "off",
88 "indirect": "off",
91 True: {
92 "exclude": "off",
93 "select": "off",
94 "hide": "off",
95 "disable": "off",
96 "render": "off",
97 "holdout": "on",
98 "indirect": "on",
103 def get_rto(layer_collection, rto):
104 if rto in ["exclude", "hide", "holdout", "indirect"]:
105 return getattr(layer_collection, rto_path[rto])
107 else:
108 collection = getattr(layer_collection, "collection")
109 return getattr(collection, rto_path[rto].split(".")[1])
112 def set_rto(layer_collection, rto, value):
113 if rto in ["exclude", "hide", "holdout", "indirect"]:
114 setattr(layer_collection, rto_path[rto], value)
116 else:
117 collection = getattr(layer_collection, "collection")
118 setattr(collection, rto_path[rto].split(".")[1], value)
121 def apply_to_children(parent, apply_function, *args, **kwargs):
122 # works for both Collections & LayerCollections
123 child_lists = [parent.children]
125 while child_lists:
126 new_child_lists = []
128 for child_list in child_lists:
129 for child in child_list:
130 apply_function(child, *args, **kwargs)
132 if child.children:
133 new_child_lists.append(child.children)
135 child_lists = new_child_lists
138 def isolate_rto(cls, self, view_layer, rto, *, children=False):
139 off = set_off_on[rto]["off"]
140 on = set_off_on[rto]["on"]
142 laycol_ptr = internals.layer_collections[self.name]["ptr"]
143 target = internals.rto_history[rto][view_layer]["target"]
144 history = internals.rto_history[rto][view_layer]["history"]
146 # get active collections
147 active_layer_collections = [x["ptr"] for x in internals.layer_collections.values()
148 if get_rto(x["ptr"], rto) == on]
150 # check if previous state should be restored
151 if cls.isolated and self.name == target:
152 # restore previous state
153 for x, item in enumerate(internals.layer_collections.values()):
154 set_rto(item["ptr"], rto, history[x])
156 # reset target and history
157 del internals.rto_history[rto][view_layer]
159 cls.isolated = False
161 # check if all RTOs should be activated
162 elif (len(active_layer_collections) == 1 and
163 active_layer_collections[0].name == self.name):
164 # activate all collections
165 for item in internals.layer_collections.values():
166 set_rto(item["ptr"], rto, on)
168 # reset target and history
169 del internals.rto_history[rto][view_layer]
171 cls.isolated = False
173 else:
174 # isolate collection
176 internals.rto_history[rto][view_layer]["target"] = self.name
178 # reset history
179 history.clear()
181 # save state
182 for item in internals.layer_collections.values():
183 history.append(get_rto(item["ptr"], rto))
185 child_states = {}
186 if children:
187 # get child states
188 def get_child_states(layer_collection):
189 child_states[layer_collection.name] = get_rto(layer_collection, rto)
191 apply_to_children(laycol_ptr, get_child_states)
193 # isolate collection
194 for item in internals.layer_collections.values():
195 if item["name"] != laycol_ptr.name:
196 set_rto(item["ptr"], rto, off)
198 set_rto(laycol_ptr, rto, on)
200 if rto not in ["exclude", "holdout", "indirect"]:
201 # activate all parents
202 laycol = internals.layer_collections[self.name]
203 while laycol["id"] != 0:
204 set_rto(laycol["ptr"], rto, on)
205 laycol = laycol["parent"]
207 if children:
208 # restore child states
209 def restore_child_states(layer_collection):
210 set_rto(layer_collection, rto, child_states[layer_collection.name])
212 apply_to_children(laycol_ptr, restore_child_states)
214 else:
215 if children:
216 # restore child states
217 def restore_child_states(layer_collection):
218 set_rto(layer_collection, rto, child_states[layer_collection.name])
220 apply_to_children(laycol_ptr, restore_child_states)
222 elif rto == "exclude":
223 # deactivate all children
224 def deactivate_all_children(layer_collection):
225 set_rto(layer_collection, rto, True)
227 apply_to_children(laycol_ptr, deactivate_all_children)
229 cls.isolated = True
232 def isolate_sel_objs_collections(view_layer, rto, caller, *, use_active=False):
233 selected_objects = get_move_selection()
235 if use_active:
236 selected_objects.add(get_move_active(always=True))
238 if not selected_objects:
239 return "No selected objects"
241 off = set_off_on[rto]["off"]
242 on = set_off_on[rto]["on"]
244 if caller == "CM":
245 history = internals.rto_history[rto+"_all"][view_layer]
247 elif caller == "QCD":
248 history = internals.qcd_history[view_layer]
251 # if not isolated, isolate collections of selected objects
252 if len(history) == 0:
253 keep_history = False
255 # save history and isolate RTOs
256 for item in internals.layer_collections.values():
257 history.append(get_rto(item["ptr"], rto))
258 rto_state = off
260 # check if any of the selected objects are in the collection
261 if not set(selected_objects).isdisjoint(item["ptr"].collection.objects):
262 rto_state = on
264 if history[-1] != rto_state:
265 keep_history = True
267 if rto == "exclude":
268 set_exclude_state(item["ptr"], rto_state)
270 else:
271 set_rto(item["ptr"], rto, rto_state)
273 # activate all parents if needed
274 if rto_state == on and rto not in ["holdout", "indirect"]:
275 laycol = item["parent"]
276 while laycol["id"] != 0:
277 set_rto(laycol["ptr"], rto, on)
278 laycol = laycol["parent"]
281 if not keep_history:
282 history.clear()
284 return "Collection already isolated"
287 else:
288 for x, item in enumerate(internals.layer_collections.values()):
289 set_rto(item["ptr"], rto, history[x])
291 # clear history
292 if caller == "CM":
293 del internals.rto_history[rto+"_all"][view_layer]
295 elif caller == "QCD":
296 del internals.qcd_history[view_layer]
299 def disable_sel_objs_collections(view_layer, rto, caller):
300 off = set_off_on[rto]["off"]
301 on = set_off_on[rto]["on"]
302 selected_objects = get_move_selection()
304 if caller == "CM":
305 history = internals.rto_history[rto+"_all"][view_layer]
307 elif caller == "QCD":
308 history = internals.qcd_history[view_layer]
311 if not selected_objects and not history:
312 # clear history
313 if caller == "CM":
314 del internals.rto_history[rto+"_all"][view_layer]
316 elif caller == "QCD":
317 del internals.qcd_history[view_layer]
319 return "No selected objects"
321 # if not disabled, disable collections of selected objects
322 if len(history) == 0:
323 # save history and disable RTOs
324 for item in internals.layer_collections.values():
325 history.append(get_rto(item["ptr"], rto))
327 # check if any of the selected objects are in the collection
328 if not set(selected_objects).isdisjoint(item["ptr"].collection.objects):
329 if rto == "exclude":
330 set_exclude_state(item["ptr"], off)
332 else:
333 set_rto(item["ptr"], rto, off)
336 else:
337 for x, item in enumerate(internals.layer_collections.values()):
338 set_rto(item["ptr"], rto, history[x])
340 # clear history
341 if caller == "CM":
342 del internals.rto_history[rto+"_all"][view_layer]
344 elif caller == "QCD":
345 del internals.qcd_history[view_layer]
348 def toggle_children(self, view_layer, rto):
349 laycol_ptr = internals.layer_collections[self.name]["ptr"]
350 # clear rto history
351 del internals.rto_history[rto][view_layer]
352 internals.rto_history[rto+"_all"].pop(view_layer, None)
354 # toggle rto state
355 state = not get_rto(laycol_ptr, rto)
356 set_rto(laycol_ptr, rto, state)
358 def set_state(layer_collection):
359 set_rto(layer_collection, rto, state)
361 apply_to_children(laycol_ptr, set_state)
364 def activate_all_rtos(view_layer, rto):
365 off = set_off_on[rto]["off"]
366 on = set_off_on[rto]["on"]
368 history = internals.rto_history[rto+"_all"][view_layer]
370 # if not activated, activate all
371 if len(history) == 0:
372 keep_history = False
374 for item in reversed(list(internals.layer_collections.values())):
375 if get_rto(item["ptr"], rto) == off:
376 keep_history = True
378 history.append(get_rto(item["ptr"], rto))
380 set_rto(item["ptr"], rto, on)
382 if not keep_history:
383 history.clear()
385 history.reverse()
387 else:
388 for x, item in enumerate(internals.layer_collections.values()):
389 set_rto(item["ptr"], rto, history[x])
391 # clear rto history
392 del internals.rto_history[rto+"_all"][view_layer]
395 def invert_rtos(view_layer, rto):
396 if rto == "exclude":
397 orig_values = []
399 for item in internals.layer_collections.values():
400 orig_values.append(get_rto(item["ptr"], rto))
402 for x, item in enumerate(internals.layer_collections.values()):
403 set_rto(item["ptr"], rto, not orig_values[x])
405 else:
406 for item in internals.layer_collections.values():
407 set_rto(item["ptr"], rto, not get_rto(item["ptr"], rto))
409 # clear rto history
410 internals.rto_history[rto].pop(view_layer, None)
413 def copy_rtos(view_layer, rto):
414 if not internals.copy_buffer["RTO"]:
415 # copy
416 internals.copy_buffer["RTO"] = rto
417 for laycol in internals.layer_collections.values():
418 internals.copy_buffer["values"].append(get_off_on[
419 get_rto(laycol["ptr"], rto)
425 else:
426 # paste
427 for x, laycol in enumerate(internals.layer_collections.values()):
428 set_rto(laycol["ptr"],
429 rto,
430 set_off_on[rto][
431 internals.copy_buffer["values"][x]
435 # clear rto history
436 internals.rto_history[rto].pop(view_layer, None)
437 del internals.rto_history[rto+"_all"][view_layer]
439 # clear copy buffer
440 internals.copy_buffer["RTO"] = ""
441 internals.copy_buffer["values"].clear()
444 def swap_rtos(view_layer, rto):
445 if not internals.swap_buffer["A"]["values"]:
446 # get A
447 internals.swap_buffer["A"]["RTO"] = rto
448 for laycol in internals.layer_collections.values():
449 internals.swap_buffer["A"]["values"].append(get_off_on[
450 get_rto(laycol["ptr"], rto)
456 else:
457 # get B
458 internals.swap_buffer["B"]["RTO"] = rto
459 for laycol in internals.layer_collections.values():
460 internals.swap_buffer["B"]["values"].append(get_off_on[
461 get_rto(laycol["ptr"], rto)
467 # swap A with B
468 for x, laycol in enumerate(internals.layer_collections.values()):
469 set_rto(laycol["ptr"], internals.swap_buffer["A"]["RTO"],
470 set_off_on[
471 internals.swap_buffer["A"]["RTO"]
473 internals.swap_buffer["B"]["values"][x]
477 set_rto(laycol["ptr"], internals.swap_buffer["B"]["RTO"],
478 set_off_on[
479 internals.swap_buffer["B"]["RTO"]
481 internals.swap_buffer["A"]["values"][x]
486 # clear rto history
487 swap_a = internals.swap_buffer["A"]["RTO"]
488 swap_b = internals.swap_buffer["B"]["RTO"]
490 internals.rto_history[swap_a].pop(view_layer, None)
491 internals.rto_history[swap_a+"_all"].pop(view_layer, None)
492 internals.rto_history[swap_b].pop(view_layer, None)
493 internals.rto_history[swap_b+"_all"].pop(view_layer, None)
495 # clear swap buffer
496 internals.swap_buffer["A"]["RTO"] = ""
497 internals.swap_buffer["A"]["values"].clear()
498 internals.swap_buffer["B"]["RTO"] = ""
499 internals.swap_buffer["B"]["values"].clear()
502 def clear_copy(rto):
503 if internals.copy_buffer["RTO"] == rto:
504 internals.copy_buffer["RTO"] = ""
505 internals.copy_buffer["values"].clear()
508 def clear_swap(rto):
509 if internals.swap_buffer["A"]["RTO"] == rto:
510 internals.swap_buffer["A"]["RTO"] = ""
511 internals.swap_buffer["A"]["values"].clear()
512 internals.swap_buffer["B"]["RTO"] = ""
513 internals.swap_buffer["B"]["values"].clear()
516 def link_child_collections_to_parent(laycol, collection, parent_collection):
517 # store view layer RTOs for all children of the to be deleted collection
518 child_states = {}
519 def get_child_states(layer_collection):
520 child_states[layer_collection.name] = (layer_collection.exclude,
521 layer_collection.hide_viewport,
522 layer_collection.holdout,
523 layer_collection.indirect_only)
525 apply_to_children(laycol["ptr"], get_child_states)
527 # link any subcollections of the to be deleted collection to it's parent
528 for subcollection in collection.children:
529 if not subcollection.name in parent_collection.children:
530 parent_collection.children.link(subcollection)
532 # apply the stored view layer RTOs to the newly linked collections and their
533 # children
534 def restore_child_states(layer_collection):
535 state = child_states.get(layer_collection.name)
537 if state:
538 layer_collection.exclude = state[0]
539 layer_collection.hide_viewport = state[1]
540 layer_collection.holdout = state[2]
541 layer_collection.indirect_only = state[3]
543 apply_to_children(laycol["parent"]["ptr"], restore_child_states)
546 def remove_collection(laycol, collection, context):
547 # get selected row
548 cm = context.scene.collection_manager
549 selected_row_name = cm.cm_list_collection[cm.cm_list_index].name
551 # delete collection
552 bpy.data.collections.remove(collection)
554 # update references
555 internals.expanded.discard(laycol["name"])
557 if internals.expand_history["target"] == laycol["name"]:
558 internals.expand_history["target"] = ""
560 if laycol["name"] in internals.expand_history["history"]:
561 internals.expand_history["history"].remove(laycol["name"])
563 if internals.qcd_slots.contains(name=laycol["name"]):
564 internals.qcd_slots.del_slot(name=laycol["name"])
566 if laycol["name"] in internals.qcd_slots.overrides:
567 internals.qcd_slots.overrides.remove(laycol["name"])
569 # reset history
570 for rto in internals.rto_history.values():
571 rto.clear()
573 # update tree view
574 update_property_group(context)
576 # update selected row
577 laycol = internals.layer_collections.get(selected_row_name, None)
578 if laycol:
579 cm.cm_list_index = laycol["row_index"]
581 elif len(cm.cm_list_collection) <= cm.cm_list_index:
582 cm.cm_list_index = len(cm.cm_list_collection) - 1
584 if cm.cm_list_index > -1:
585 name = cm.cm_list_collection[cm.cm_list_index].name
586 laycol = internals.layer_collections[name]
587 while not laycol["visible"]:
588 laycol = laycol["parent"]
590 cm.cm_list_index = laycol["row_index"]
593 def select_collection_objects(is_master_collection, collection_name, replace, nested, selection_state=None):
594 if bpy.context.mode != 'OBJECT':
595 return
597 if is_master_collection:
598 target_collection = bpy.context.view_layer.layer_collection.collection
600 else:
601 laycol = internals.layer_collections[collection_name]
602 target_collection = laycol["ptr"].collection
604 if replace:
605 bpy.ops.object.select_all(action='DESELECT')
607 def select_objects(collection, selection_state):
608 if selection_state == None:
609 selection_state = get_move_selection().isdisjoint(collection.objects)
611 for obj in collection.objects:
612 try:
613 obj.select_set(selection_state)
614 except RuntimeError:
615 pass
617 select_objects(target_collection, selection_state)
619 if nested:
620 apply_to_children(target_collection, select_objects, selection_state)
622 def set_exclude_state(target_layer_collection, state):
623 # get current child exclusion state
624 child_exclusion = []
626 def get_child_exclusion(layer_collection):
627 child_exclusion.append([layer_collection, layer_collection.exclude])
629 apply_to_children(target_layer_collection, get_child_exclusion)
632 # set exclusion
633 target_layer_collection.exclude = state
636 # set correct state for all children
637 for laycol in child_exclusion:
638 laycol[0].exclude = laycol[1]