Newer
Older
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# Copyright 2011, Ryan Inch
import bpy
from bpy.types import (
Operator,
)
from bpy.props import (
BoolProperty,
StringProperty,
IntProperty
)
from . import internals
from .internals import (
update_property_group,
get_move_active,
update_qcd_header,
from .operator_utils import (
select_collection_objects,
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
class LockedObjects():
def __init__(self):
self.objs = []
self.mode = ""
def get_locked_objs(context):
# get objects not in object mode
locked = LockedObjects()
if context.mode == 'OBJECT':
return locked
if context.view_layer.objects.active:
active = context.view_layer.objects.active
locked.mode = mode_converter[context.mode]
for obj in context.view_layer.objects:
if obj.mode != 'OBJECT':
if obj.mode not in ['POSE', 'WEIGHT_PAINT'] or obj == active:
if obj.mode == active.mode:
locked.objs.append(obj)
return locked
class QCDAllBase():
meta_op = False
meta_report = None
context = None
view_layer = ""
history = None
orig_active_collection = None
orig_active_object = None
locked = None
@classmethod
def init(cls, context):
cls.context = context
cls.orig_active_collection = context.view_layer.active_layer_collection
cls.view_layer = context.view_layer.name
cls.orig_active_object = context.view_layer.objects.active
if not cls.view_layer in internals.qcd_history:
internals.qcd_history[cls.view_layer] = []
cls.history = internals.qcd_history[cls.view_layer]
cls.locked = get_locked_objs(context)
@classmethod
def apply_history(cls):
for x, item in enumerate(internals.layer_collections.values()):
item["ptr"].exclude = cls.history[x]
# clear rto history
del internals.qcd_history[cls.view_layer]
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
internals.qcd_collection_state.clear()
cls.history = None
@classmethod
def finalize(cls):
# restore active collection
cls.context.view_layer.active_layer_collection = cls.orig_active_collection
# restore active object if possible
if cls.orig_active_object:
if cls.orig_active_object.name in cls.context.view_layer.objects:
cls.context.view_layer.objects.active = cls.orig_active_object
# restore locked objects back to their original mode
# needed because of exclude child updates
if cls.context.view_layer.objects.active:
if cls.locked.objs:
bpy.ops.object.mode_set(mode=cls.locked.mode)
@classmethod
def clear(cls):
cls.context = None
cls.view_layer = ""
cls.history = None
cls.orig_active_collection = None
cls.orig_active_object = None
cls.locked = {}
class EnableAllQCDSlotsMeta(Operator):
'''QCD All Meta Operator'''
bl_label = "Quick View Toggles"
bl_description = (
" * LMB - Enable all slots/Restore.\n"
" * Alt+LMB - Select all objects in QCD slots.\n"
" * LMB+Hold - Menu"
)
bl_idname = "view3d.enable_all_qcd_slots_meta"
def invoke(self, context, event):
qab = QCDAllBase
modifiers = get_modifiers(event)
qab.meta_op = True
if modifiers == {"alt"}:
bpy.ops.view3d.select_all_qcd_objects()
else:
qab.init(context)
if not qab.history:
bpy.ops.view3d.enable_all_qcd_slots()
else:
qab.apply_history()
qab.finalize()
if qab.meta_report:
self.report({"INFO"}, qab.meta_report)
qab.meta_report = None
qab.meta_op = False
return {'FINISHED'}
class EnableAllQCDSlots(Operator):
'''Toggles between the current state and all enabled'''
bl_label = "Enable All QCD Slots"
bl_idname = "view3d.enable_all_qcd_slots"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
qab = QCDAllBase
# validate qcd slots
if not dict(internals.qcd_slots):
if qab.meta_op:
qab.meta_report = "No QCD slots."
else:
self.report({"INFO"}, "No QCD slots.")
return {'CANCELLED'}
qab.init(context)
if not qab.history:
keep_history = False
for laycol in internals.layer_collections.values():
is_qcd_slot = internals.qcd_slots.contains(name=laycol["name"])
qab.history.append(laycol["ptr"].exclude)
if is_qcd_slot and laycol["ptr"].exclude:
keep_history = True
set_exclude_state(laycol["ptr"], False)
if not keep_history:
# clear rto history
del internals.qcd_history[qab.view_layer]
qab.clear()
if qab.meta_op:
qab.meta_report = "All QCD slots are already enabled."
else:
self.report({"INFO"}, "All QCD slots are already enabled.")
return {'CANCELLED'}
internals.qcd_collection_state.clear()
internals.qcd_collection_state.update(internals.generate_state(qcd=True))
else:
qab.apply_history()
qab.finalize()
return {'FINISHED'}
class EnableAllQCDSlotsIsolated(Operator):
'''Toggles between the current state and all enabled (non-QCD collections disabled)'''
bl_label = "Enable All QCD Slots Isolated"
bl_idname = "view3d.enable_all_qcd_slots_isolated"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
qab = QCDAllBase
# validate qcd slots
if not dict(internals.qcd_slots):
self.report({"INFO"}, "No QCD slots.")
return {'CANCELLED'}
qab.init(context)
if qab.locked.objs and not internals.qcd_slots.object_in_slots(qab.orig_active_object):
del internals.qcd_history[qab.view_layer]
qab.clear()
self.report({"WARNING"}, "Cannot execute. The active object would be lost.")
return {'CANCELLED'}
if not qab.history:
keep_history = False
for laycol in internals.layer_collections.values():
is_qcd_slot = internals.qcd_slots.contains(name=laycol["name"])
qab.history.append(laycol["ptr"].exclude)
if is_qcd_slot and laycol["ptr"].exclude:
keep_history = True
set_exclude_state(laycol["ptr"], False)
if not is_qcd_slot and not laycol["ptr"].exclude:
keep_history = True
set_exclude_state(laycol["ptr"], True)
if not keep_history:
# clear rto history
del internals.qcd_history[qab.view_layer]
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
qab.clear()
self.report({"INFO"}, "All QCD slots are already enabled and isolated.")
return {'CANCELLED'}
internals.qcd_collection_state.clear()
internals.qcd_collection_state.update(internals.generate_state(qcd=True))
else:
qab.apply_history()
qab.finalize()
return {'FINISHED'}
class DisableAllNonQCDSlots(Operator):
'''Toggles between the current state and all non-QCD collections disabled'''
bl_label = "Disable All Non QCD Slots"
bl_idname = "view3d.disable_all_non_qcd_slots"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
qab = QCDAllBase
# validate qcd slots
if not dict(internals.qcd_slots):
self.report({"INFO"}, "No QCD slots.")
return {'CANCELLED'}
qab.init(context)
if qab.locked.objs and not internals.qcd_slots.object_in_slots(qab.orig_active_object):
del internals.qcd_history[qab.view_layer]
qab.clear()
self.report({"WARNING"}, "Cannot execute. The active object would be lost.")
return {'CANCELLED'}
if not qab.history:
keep_history = False
for laycol in internals.layer_collections.values():
is_qcd_slot = internals.qcd_slots.contains(name=laycol["name"])
qab.history.append(laycol["ptr"].exclude)
if not is_qcd_slot and not laycol["ptr"].exclude:
keep_history = True
set_exclude_state(laycol["ptr"], True)
if not keep_history:
# clear rto history
del internals.qcd_history[qab.view_layer]
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
qab.clear()
self.report({"INFO"}, "All non QCD slots are already disabled.")
return {'CANCELLED'}
internals.qcd_collection_state.clear()
internals.qcd_collection_state.update(internals.generate_state(qcd=True))
else:
qab.apply_history()
qab.finalize()
return {'FINISHED'}
class DisableAllCollections(Operator):
'''Toggles between the current state and all collections disabled'''
bl_label = "Disable All collections"
bl_idname = "view3d.disable_all_collections"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
qab = QCDAllBase
qab.init(context)
if qab.locked.objs:
# clear rto history
del internals.qcd_history[qab.view_layer]
qab.clear()
self.report({"WARNING"}, "Cannot execute. The active object would be lost.")
return {'CANCELLED'}
if not qab.history:
for laycol in internals.layer_collections.values():
qab.history.append(laycol["ptr"].exclude)
if all(qab.history): # no collections are enabled
# clear rto history
del internals.qcd_history[qab.view_layer]
qab.clear()
self.report({"INFO"}, "All collections are already disabled.")
return {'CANCELLED'}
for laycol in internals.layer_collections.values():
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
laycol["ptr"].exclude = True
internals.qcd_collection_state.clear()
internals.qcd_collection_state.update(internals.generate_state(qcd=True))
else:
qab.apply_history()
qab.finalize()
return {'FINISHED'}
class SelectAllQCDObjects(Operator):
'''Select all objects in QCD slots'''
bl_label = "Select All QCD Objects"
bl_idname = "view3d.select_all_qcd_objects"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
qab = QCDAllBase
if context.mode != 'OBJECT':
return {'CANCELLED'}
if not context.selectable_objects:
if qab.meta_op:
qab.meta_report = "No objects present to select."
else:
self.report({"INFO"}, "No objects present to select.")
return {'CANCELLED'}
orig_selected_objects = context.selected_objects
bpy.ops.object.select_all(action='DESELECT')
for slot, collection_name in internals.qcd_slots:
select_collection_objects(
is_master_collection=False,
collection_name=collection_name,
replace=False,
nested=False,
selection_state=True
)
if context.selected_objects == orig_selected_objects:
for slot, collection_name in internals.qcd_slots:
select_collection_objects(
is_master_collection=False,
collection_name=collection_name,
replace=False,
nested=False,
selection_state=False
)
return {'FINISHED'}
class DiscardQCDHistory(Operator):
'''Discard QCD History'''
bl_label = "Discard History"
bl_idname = "view3d.discard_qcd_history"
def execute(self, context):
qab = QCDAllBase
view_layer = context.view_layer.name
if view_layer in internals.qcd_history:
del internals.qcd_history[view_layer]
qab.clear()
return {'FINISHED'}
class MoveToQCDSlot(Operator):
'''Move object(s) to QCD slot'''
bl_label = "Move To QCD Slot"
bl_idname = "view3d.move_to_qcd_slot"
bl_options = {'REGISTER', 'UNDO'}
slot: StringProperty()
toggle: BoolProperty()
def execute(self, context):
selected_objects = get_move_selection()
active_object = get_move_active()
internals.move_triggered = True
slot_name = internals.qcd_slots.get_name(self.slot)
qcd_laycol = internals.layer_collections[slot_name]["ptr"]
else:
return {'CANCELLED'}
if not selected_objects:
return {'CANCELLED'}
# adds object to slot
if self.toggle:
if not active_object:
active_object = tuple(selected_objects)[0]
if not active_object.name in qcd_laycol.collection.objects:
for obj in selected_objects:
if obj.name not in qcd_laycol.collection.objects:
qcd_laycol.collection.objects.link(obj)
else:
for obj in selected_objects:
if obj.name in qcd_laycol.collection.objects:
if len(obj.users_collection) == 1:
continue
qcd_laycol.collection.objects.unlink(obj)
# moves object to slot
else:
for obj in selected_objects:
if obj.name not in qcd_laycol.collection.objects:
qcd_laycol.collection.objects.link(obj)
for collection in obj.users_collection:
qcd_idx = internals.qcd_slots.get_idx(collection.name)
if qcd_idx != self.slot:
collection.objects.unlink(obj)
# update the active object if needed
if not context.active_object:
try:
context.view_layer.objects.active = active_object
except RuntimeError: # object not in visible slot
pass
# update header UI
return {'FINISHED'}
class ViewMoveQCDSlot(Operator):
bl_idname = "view3d.view_move_qcd_slot"
bl_options = {'REGISTER', 'UNDO'}
slot: StringProperty()
@classmethod
def description(cls, context, properties):
slot_name = qcd_slots.get_name(properties.slot)
slot_string = f"QCD Slot {properties.slot}: \"{slot_name}\"\n"
hotkey_string = (
" * LMB - Isolate slot.\n"
" * Shift+LMB - Toggle slot.\n"
" * Ctrl+LMB - Move objects to slot.\n"
" * Ctrl+Shift+LMB - Toggle objects' slot.\n"
" * Alt+LMB - Select objects in slot.\n"
" * Alt+Shift+LMB - Toggle objects' selection for slot"
)
return f"{slot_string}{hotkey_string}"
def invoke(self, context, event):
modifiers = get_modifiers(event)
if modifiers == {"shift"}:
bpy.ops.view3d.view_qcd_slot(slot=self.slot, toggle=True)
elif modifiers == {"ctrl"}:
bpy.ops.view3d.move_to_qcd_slot(slot=self.slot, toggle=False)
elif modifiers == {"ctrl", "shift"}:
bpy.ops.view3d.move_to_qcd_slot(slot=self.slot, toggle=True)
elif modifiers == {"alt"}:
select_collection_objects(
collection_name=internals.qcd_slots.get_name(self.slot),
replace=True,
nested=False
)
elif modifiers == {"alt", "shift"}:
select_collection_objects(
collection_name=internals.qcd_slots.get_name(self.slot),
replace=False,
nested=False
)
else:
bpy.ops.view3d.view_qcd_slot(slot=self.slot, toggle=False)
class ViewQCDSlot(Operator):
'''View objects in QCD slot'''
bl_label = "View QCD Slot"
bl_idname = "view3d.view_qcd_slot"
slot: StringProperty()
toggle: BoolProperty()
def execute(self, context):
slot_name = internals.qcd_slots.get_name(self.slot)
qcd_laycol = internals.layer_collections[slot_name]["ptr"]
else:
return {'CANCELLED'}
orig_active_object = context.view_layer.objects.active
locked = get_locked_objs(context)
# check if slot can be toggled off.
if not qcd_laycol.exclude:
if not set(locked.objs).isdisjoint(qcd_laycol.collection.objects):
return {'CANCELLED'}
# toggle exclusion of qcd_laycol
set_exclude_state(qcd_laycol, not qcd_laycol.exclude)
# exclude all collections
for laycol in internals.layer_collections.values():
if laycol["name"] != qcd_laycol.name:
# prevent exclusion if locked objects in this collection
if set(locked.objs).isdisjoint(laycol["ptr"].collection.objects):
laycol["ptr"].exclude = True
else:
laycol["ptr"].exclude = False
# un-exclude target collection
qcd_laycol.exclude = False
# exclude all children
def exclude_all_children(layer_collection):
# prevent exclusion if locked objects in this collection
if set(locked.objs).isdisjoint(layer_collection.collection.objects):
layer_collection.exclude = True
else:
layer_collection.exclude = False
apply_to_children(qcd_laycol, exclude_all_children)
if orig_active_object:
if orig_active_object.name in context.view_layer.objects:
context.view_layer.objects.active = orig_active_object
# restore locked objects back to their original mode
# needed because of exclude child updates
if context.view_layer.objects.active:
if locked.objs:
bpy.ops.object.mode_set(mode=locked.mode)
# set layer as active layer collection
context.view_layer.active_layer_collection = qcd_laycol
view_layer = context.view_layer.name
if view_layer in internals.rto_history["exclude"]:
del internals.rto_history["exclude"][view_layer]
if view_layer in internals.rto_history["exclude_all"]:
del internals.rto_history["exclude_all"][view_layer]
return {'FINISHED'}
class RenumerateQCDSlots(Operator):
bl_label = "Renumber QCD Slots"
"Renumber QCD slots.\n"
" * LMB - Renumber (breadth first) from slot 1.\n"
" * +Ctrl - Linear.\n"
" * +Alt - Reset.\n"
" * +Shift - Constrain to branch"
bl_idname = "view3d.renumerate_qcd_slots"
bl_options = {'REGISTER', 'UNDO'}
def invoke(self, context, event):
modifiers = get_modifiers(event)
beginning = False
depth_first = False
constrain = False
if 'alt' in modifiers:
beginning=True
if 'ctrl' in modifiers:
depth_first=True
if 'shift' in modifiers:
constrain=True
internals.qcd_slots.renumerate(beginning=beginning,
depth_first=depth_first,
constrain=constrain)
update_property_group(context)