glTF exporter: Fix Real children of instance collection visibility check
[blender-addons.git] / object_boolean_tools.py
bloba1090ce5e218801ae2580de1bdfbf094a7ddeb65
1 # SPDX-FileCopyrightText: 2016-2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 bl_info = {
6 "name": "Bool Tool",
7 "author": "Vitor Balbio, Mikhail Rachinskiy, TynkaTopi, Meta-Androcto, Simon Appelt",
8 "version": (0, 4, 1),
9 "blender": (2, 80, 0),
10 "location": "View3D > Sidebar > Edit Tab",
11 "description": "Bool Tool Hotkey: Ctrl Shift B",
12 "doc_url": "{BLENDER_MANUAL_URL}/addons/object/bool_tools.html",
13 "category": "Object",
16 import bpy
17 from bpy.types import (
18 AddonPreferences,
19 Operator,
20 Panel,
21 Menu,
23 from bpy.props import (
24 BoolProperty,
25 StringProperty,
29 # ------------------- Bool Tool FUNCTIONS -------------------------
30 # Utils:
32 # Hide boolean objects
33 def update_BoolHide(self, context):
34 ao = context.view_layer.objects.active
35 objs = [i.object for i in ao.modifiers if i.type == "BOOLEAN"]
36 hide_state = context.scene.BoolHide
38 for o in objs:
39 o.hide_viewport = hide_state
42 def isCanvas(_obj):
43 try:
44 if _obj["BoolToolRoot"]:
45 return True
46 except:
47 return False
50 def isBrush(_obj):
51 try:
52 if _obj["BoolToolBrush"]:
53 return True
54 except:
55 return False
58 # TODO
59 # def isPolyBrush(_obj):
60 # try:
61 # if _obj["BoolToolPolyBrush"]:
62 # return True
63 # except:
64 # return False
67 def object_visibility_set(ob, value=False):
68 ob.visible_camera = value
69 ob.visible_diffuse = value
70 ob.visible_glossy = value
71 ob.visible_shadow = value
72 ob.visible_transmission = value
73 ob.visible_volume_scatter = value
76 def BT_ObjectByName(obj):
77 for ob in bpy.context.view_layer.objects:
78 if isCanvas(ob) or isBrush(ob):
79 if ob.name == obj:
80 return ob
83 def FindCanvas(obj):
84 for ob in bpy.context.view_layer.objects:
85 if isCanvas(ob):
86 for mod in ob.modifiers:
87 if "BTool_" in mod.name:
88 if obj.name in mod.name:
89 return ob
92 def isFTransf():
93 preferences = bpy.context.preferences
94 addons = preferences.addons
95 addon_prefs = addons[__name__].preferences
96 if addon_prefs.fast_transform:
97 return True
98 else:
99 return False
102 def ConvertToMesh(obj):
103 act = bpy.context.view_layer.objects.active
104 bpy.context.view_layer.objects.active = obj
105 bpy.ops.object.convert(target="MESH")
106 bpy.context.view_layer.objects.active = act
109 # Do the Union, Difference and Intersection Operations with a Brush
110 def Operation(context, _operation):
111 prefs = context.preferences.addons[__name__].preferences
112 useWire = prefs.use_wire
114 for selObj in context.selected_objects:
115 if (
116 selObj != context.active_object and
117 (selObj.type == "MESH" or selObj.type == "CURVE")
119 if selObj.type == "CURVE":
120 ConvertToMesh(selObj)
121 actObj = context.active_object
122 selObj.hide_render = True
124 if useWire:
125 selObj.display_type = "WIRE"
126 else:
127 selObj.display_type = "BOUNDS"
129 object_visibility_set(selObj, value=False)
131 if _operation == "SLICE":
132 # copies instance_collection property(empty), but group property is empty (users_group = None)
133 clone = actObj.copy()
134 context.collection.objects.link(clone)
136 space_data = context.space_data
137 is_local_view = bool(space_data.local_view)
139 if is_local_view:
140 clone.local_view_set(space_data, True)
142 sliceMod = clone.modifiers.new("BTool_" + selObj.name, "BOOLEAN") # add mod to clone obj
143 sliceMod.object = selObj
144 sliceMod.operation = "DIFFERENCE"
145 clone["BoolToolRoot"] = True
147 newMod = actObj.modifiers.new("BTool_" + selObj.name, "BOOLEAN")
148 newMod.object = selObj
150 if _operation == "SLICE":
151 newMod.operation = "INTERSECT"
152 else:
153 newMod.operation = _operation
155 actObj["BoolToolRoot"] = True
156 selObj["BoolToolBrush"] = _operation
157 selObj["BoolTool_FTransform"] = "False"
160 # Remove Objects form the BoolTool System
161 def Remove(context, thisObj_name, Prop):
162 # Find the Brush pointed in the Tree View and Restore it, active is the Canvas
163 actObj = context.active_object
165 # Restore the Brush
166 def RemoveThis(_thisObj_name):
167 for obj in bpy.context.view_layer.objects:
168 # if it's the brush object
169 if obj.name == _thisObj_name:
170 obj.display_type = "TEXTURED"
171 del obj["BoolToolBrush"]
172 del obj["BoolTool_FTransform"]
173 object_visibility_set(obj, value=True)
175 # Remove it from the Canvas
176 for mod in actObj.modifiers:
177 if "BTool_" in mod.name:
178 if _thisObj_name in mod.name:
179 actObj.modifiers.remove(mod)
181 if Prop == "THIS":
182 RemoveThis(thisObj_name)
184 # If the remove was called from the Properties:
185 else:
186 # Remove the Brush Property
187 if Prop == "BRUSH":
188 Canvas = FindCanvas(actObj)
190 if Canvas:
191 for mod in Canvas.modifiers:
192 if "BTool_" in mod.name and actObj.name in mod.name:
193 Canvas.modifiers.remove(mod)
195 actObj.display_type = "TEXTURED"
196 del actObj["BoolToolBrush"]
197 del actObj["BoolTool_FTransform"]
198 object_visibility_set(actObj, value=True)
200 if Prop == "CANVAS":
201 for mod in actObj.modifiers:
202 if "BTool_" in mod.name:
203 RemoveThis(mod.object.name)
206 # Toggle the Enable the Brush Object Property
207 def EnableBrush(context, objList, canvas):
208 for obj in objList:
209 for mod in canvas.modifiers:
210 if "BTool_" in mod.name and mod.object.name == obj:
212 if mod.show_viewport:
213 mod.show_viewport = False
214 mod.show_render = False
215 else:
216 mod.show_viewport = True
217 mod.show_render = True
220 # Find the Canvas and Enable this Brush
221 def EnableThisBrush(context, set):
222 canvas = None
223 for obj in bpy.context.view_layer.objects:
224 if obj != bpy.context.active_object:
225 if isCanvas(obj):
226 for mod in obj.modifiers:
227 if "BTool_" in mod.name:
228 if mod.object == bpy.context.active_object:
229 canvas = obj
231 for mod in canvas.modifiers:
232 if "BTool_" in mod.name:
233 if mod.object == bpy.context.active_object:
234 if set == "None":
235 if mod.show_viewport:
236 mod.show_viewport = False
237 mod.show_render = False
238 else:
239 mod.show_viewport = True
240 mod.show_render = True
241 else:
242 if set == "True":
243 mod.show_viewport = True
244 else:
245 mod.show_viewport = False
246 return
249 # Toggle the Fast Transform Property of the Active Brush
250 def EnableFTransf(context):
251 actObj = bpy.context.active_object
253 if actObj["BoolTool_FTransform"] == "True":
254 actObj["BoolTool_FTransform"] = "False"
255 else:
256 actObj["BoolTool_FTransform"] = "True"
257 return
260 # Apply All Brushes to the Canvas
261 def ApplyAll(context, list):
262 objDeleteList = []
263 for selObj in list:
264 if isCanvas(selObj) and selObj == context.active_object:
265 for mod in selObj.modifiers:
266 if "BTool_" in mod.name:
267 objDeleteList.append(mod.object)
268 try:
269 bpy.ops.object.modifier_apply(modifier=mod.name)
270 except: # if fails the means it is multiuser data
271 context.active_object.data = context.active_object.data.copy() # so just make data unique
272 bpy.ops.object.modifier_apply(modifier=mod.name)
273 del selObj["BoolToolRoot"]
275 for obj in context.scene.objects:
276 if isCanvas(obj):
277 for mod in obj.modifiers:
278 # do not delete brush that is used by another canvas
279 if mod.type == "BOOLEAN" and mod.object in objDeleteList:
280 objDeleteList.remove(mod.object) # remove it from deletion
282 bpy.ops.object.select_all(action="DESELECT")
283 for obj in objDeleteList:
284 obj.select_set(True)
285 bpy.ops.object.delete()
288 # Apply This Brush to the Canvas
289 def ApplyThisBrush(context, brush):
290 for obj in context.scene.objects:
291 if isCanvas(obj):
292 for mod in obj.modifiers:
293 if "BTool_" + brush.name in mod.name:
294 # Apply This Brush
295 context.view_layer.objects.active = obj
296 try:
297 bpy.ops.object.modifier_apply(modifier=mod.name)
298 except: # if fails the means it is multiuser data
299 context.active_object.data = context.active_object.data.copy() # so just make data unique
300 bpy.ops.object.modifier_apply(modifier=mod.name)
301 bpy.ops.object.select_all(action="TOGGLE")
302 bpy.ops.object.select_all(action="DESELECT")
304 # Garbage Collector
305 brush.select_set(True)
306 # bpy.ops.object.delete()
309 # ------------------ Bool Tool OPERATORS --------------------------------------
311 # TODO
312 # class BTool_DrawPolyBrush(Operator):
313 # bl_idname = "btool.draw_polybrush"
314 # bl_label = "Draw Poly Brush"
315 # bl_description = (
316 # "Draw Polygonal Mask, can be applied to Canvas > Brush or Directly\n"
317 # "Note: ESC to Cancel, Enter to Apply, Right Click to erase the Lines"
320 # count = 0
321 # store_cont_draw = False
323 # @classmethod
324 # def poll(cls, context):
325 # return context.active_object is not None
327 # def set_cont_draw(self, context, start=False):
328 # # store / restore GP continuous drawing (see T52321)
329 # scene = context.scene
330 # tool_settings = scene.tool_settings
331 # continuous = tool_settings.use_gpencil_continuous_drawing
332 # if start:
333 # self.store_cont_draw = continuous
334 # tool_settings.use_gpencil_continuous_drawing = True
335 # else:
336 # tool_settings.use_gpencil_continuous_drawing = self.store_cont_draw
338 # def modal(self, context, event):
339 # self.count += 1
340 # actObj = bpy.context.active_object
341 # if self.count == 1:
342 # actObj.select_set(True)
343 # bpy.ops.gpencil.draw("INVOKE_DEFAULT", mode="DRAW_POLY")
345 # if event.type == "RIGHTMOUSE":
346 # # use this to pass to the Grease Pencil eraser (see T52321)
347 # pass
349 # if event.type in {"RET", "NUMPAD_ENTER"}:
351 # bpy.ops.gpencil.convert(type="POLY")
352 # self.set_cont_draw(context)
354 # for obj in context.selected_objects:
355 # if obj.type == "CURVE":
356 # obj.name = "PolyDraw"
357 # bpy.context.view_layer.objects.active = obj
358 # bpy.ops.object.select_all(action="DESELECT")
359 # obj.select_set(True)
360 # bpy.ops.object.convert(target="MESH")
361 # bpy.ops.object.mode_set(mode="EDIT")
362 # bpy.ops.mesh.select_all(action="SELECT")
363 # bpy.ops.mesh.edge_face_add()
364 # bpy.ops.mesh.flip_normals()
365 # bpy.ops.object.mode_set(mode="OBJECT")
366 # bpy.ops.object.origin_set(type="ORIGIN_CENTER_OF_MASS")
367 # bpy.ops.object.modifier_add(type="SOLIDIFY")
368 # for mod in obj.modifiers:
369 # if mod.name == "Solidify":
370 # mod.name = "BTool_PolyBrush"
371 # mod.thickness = 1
372 # mod.offset = 0
373 # obj["BoolToolPolyBrush"] = True
375 # bpy.ops.object.select_all(action="DESELECT")
376 # bpy.context.view_layer.objects.active = actObj
377 # bpy.context.view_layer.update()
378 # actObj.select_set(True)
379 # obj.select_set(True)
381 # bpy.context.view_layer.grease_pencil.clear()
382 # bpy.ops.gpencil.data_unlink()
384 # return {"FINISHED"}
386 # if event.type == "ESC":
387 # bpy.ops.ed.undo() # remove o Grease Pencil
388 # self.set_cont_draw(context)
390 # self.report({"INFO"}, "Draw Poly Brush: Operation Cancelled by User")
391 # return {"CANCELLED"}
393 # return {"RUNNING_MODAL"}
395 # def invoke(self, context, event):
396 # if context.object:
397 # self.set_cont_draw(context, start=True)
398 # context.window_manager.modal_handler_add(self)
399 # return {"RUNNING_MODAL"}
400 # else:
401 # self.report({"WARNING"}, "No active object, could not finish")
402 # return {"CANCELLED"}
405 # Fast Transform
406 class BTool_FastTransform(Operator):
407 bl_idname = "btool.fast_transform"
408 bl_label = "Fast Transform"
409 bl_description = "Enable Fast Transform"
411 operator: StringProperty("")
413 count = 0
415 def modal(self, context, event):
416 self.count += 1
417 actObj = bpy.context.active_object
418 useWire = bpy.context.preferences.addons[__name__].preferences.use_wire
419 if self.count == 1:
421 if isBrush(actObj) and actObj["BoolTool_FTransform"] == "True":
422 EnableThisBrush(bpy.context, "False")
423 if useWire:
424 actObj.display_type = "WIRE"
425 else:
426 actObj.display_type = "BOUNDS"
428 if self.operator == "Translate":
429 bpy.ops.transform.translate("INVOKE_DEFAULT")
430 if self.operator == "Rotate":
431 bpy.ops.transform.rotate("INVOKE_DEFAULT")
432 if self.operator == "Scale":
433 bpy.ops.transform.resize("INVOKE_DEFAULT")
435 if event.type == "LEFTMOUSE":
436 if isBrush(actObj):
437 EnableThisBrush(bpy.context, "True")
438 actObj.display_type = "WIRE"
439 return {"FINISHED"}
441 if event.type in {"RIGHTMOUSE", "ESC"}:
442 if isBrush(actObj):
443 EnableThisBrush(bpy.context, "True")
444 actObj.display_type = "WIRE"
445 return {"CANCELLED"}
447 return {"RUNNING_MODAL"}
449 def invoke(self, context, event):
450 if context.object:
451 context.window_manager.modal_handler_add(self)
452 return {"RUNNING_MODAL"}
453 else:
454 self.report({"WARNING"}, "No active object, could not finish")
455 return {"CANCELLED"}
458 # ------------------- Bool Tool OPERATOR CLASSES --------------------------------------------------------
461 # Brush operators
462 # --------------------------------------------------------------------------------------
465 class BToolSetup():
467 def execute(self, context):
468 Operation(context, self.mode)
469 return {"FINISHED"}
471 def invoke(self, context, event):
472 if len(context.selected_objects) < 2:
473 self.report({"ERROR"}, "At least two objects must be selected")
474 return {"CANCELLED"}
476 return self.execute(context)
479 class BTool_Union(Operator, BToolSetup):
480 bl_idname = "btool.boolean_union"
481 bl_label = "Brush Union"
482 bl_description = "This operator add a union brush to a canvas"
483 bl_options = {"REGISTER", "UNDO"}
485 mode = "UNION"
488 class BTool_Inters(Operator, BToolSetup):
489 bl_idname = "btool.boolean_inters"
490 bl_label = "Brush Intersection"
491 bl_description = "This operator add a intersect brush to a canvas"
492 bl_options = {"REGISTER", "UNDO"}
494 mode = "INTERSECT"
497 class BTool_Diff(Operator, BToolSetup):
498 bl_idname = "btool.boolean_diff"
499 bl_label = "Brush Difference"
500 bl_description = "This operator add a difference brush to a canvas"
501 bl_options = {"REGISTER", "UNDO"}
503 mode = "DIFFERENCE"
506 class BTool_Slice(Operator, BToolSetup):
507 bl_idname = "btool.boolean_slice"
508 bl_label = "Brush Slice"
509 bl_description = "This operator add a intersect brush to a canvas"
510 bl_options = {"REGISTER", "UNDO"}
512 mode = "SLICE"
515 # Auto Boolean operators
516 # --------------------------------------------------------------------------------------
519 class Auto_Boolean:
521 def objects_prepare(self):
522 for ob in bpy.context.selected_objects:
523 if ob.type != "MESH":
524 ob.select_set(False)
525 bpy.ops.object.make_single_user(object=True, obdata=True)
526 bpy.ops.object.convert(target="MESH")
528 def mesh_selection(self, ob, select_action):
529 obj = bpy.context.active_object
531 bpy.context.view_layer.objects.active = ob
532 bpy.ops.object.mode_set(mode="EDIT")
534 bpy.ops.mesh.reveal()
535 bpy.ops.mesh.select_all(action=select_action)
537 bpy.ops.object.mode_set(mode="OBJECT")
538 bpy.context.view_layer.objects.active = obj
540 def boolean_operation(self):
541 obj = bpy.context.active_object
542 obj.select_set(False)
543 obs = bpy.context.selected_objects
545 self.mesh_selection(obj, "DESELECT")
547 for ob in obs:
548 self.mesh_selection(ob, "SELECT")
549 self.boolean_mod(obj, ob, self.mode)
551 obj.select_set(True)
553 def boolean_mod(self, obj, ob, mode, ob_delete=True):
554 md = obj.modifiers.new("Auto Boolean", "BOOLEAN")
555 md.show_viewport = False
556 md.operation = mode
557 md.object = ob
559 context_override = {'object': obj}
560 with bpy.context.temp_override(**context_override):
561 bpy.ops.object.modifier_apply(modifier=md.name)
563 if ob_delete:
564 bpy.data.objects.remove(ob)
566 def execute(self, context):
567 self.objects_prepare()
568 self.boolean_operation()
569 return {"FINISHED"}
571 def invoke(self, context, event):
572 if len(context.selected_objects) < 2:
573 self.report({"ERROR"}, "At least two objects must be selected")
574 return {"CANCELLED"}
576 return self.execute(context)
579 class OBJECT_OT_BoolTool_Auto_Union(Operator, Auto_Boolean):
580 bl_idname = "object.booltool_auto_union"
581 bl_label = "Bool Tool Union"
582 bl_description = "Combine selected objects"
583 bl_options = {"REGISTER", "UNDO"}
585 mode = "UNION"
588 class OBJECT_OT_BoolTool_Auto_Difference(Operator, Auto_Boolean):
589 bl_idname = "object.booltool_auto_difference"
590 bl_label = "Bool Tool Difference"
591 bl_description = "Subtract selected objects from active object"
592 bl_options = {"REGISTER", "UNDO"}
594 mode = "DIFFERENCE"
597 class OBJECT_OT_BoolTool_Auto_Intersect(Operator, Auto_Boolean):
598 bl_idname = "object.booltool_auto_intersect"
599 bl_label = "Bool Tool Intersect"
600 bl_description = "Keep only intersecting geometry"
601 bl_options = {"REGISTER", "UNDO"}
603 mode = "INTERSECT"
606 class OBJECT_OT_BoolTool_Auto_Slice(Operator, Auto_Boolean):
607 bl_idname = "object.booltool_auto_slice"
608 bl_label = "Bool Tool Slice"
609 bl_description = "Slice active object along the selected objects"
610 bl_options = {"REGISTER", "UNDO"}
612 def execute(self, context):
613 space_data = context.space_data
614 is_local_view = bool(space_data.local_view)
615 self.objects_prepare()
617 ob1 = context.active_object
618 ob1.select_set(False)
619 self.mesh_selection(ob1, "DESELECT")
621 for ob2 in context.selected_objects:
623 self.mesh_selection(ob2, "SELECT")
625 ob1_copy = ob1.copy()
626 ob1_copy.data = ob1.data.copy()
628 for coll in ob1.users_collection:
629 coll.objects.link(ob1_copy)
631 if is_local_view:
632 ob1_copy.local_view_set(space_data, True)
634 self.boolean_mod(ob1, ob2, "DIFFERENCE", ob_delete=False)
635 self.boolean_mod(ob1_copy, ob2, "INTERSECT")
636 ob1_copy.select_set(True)
638 context.view_layer.objects.active = ob1_copy
640 return {"FINISHED"}
643 # Utils Class ---------------------------------------------------------------
645 # Find the Brush Selected in Three View
646 class BTool_FindBrush(Operator):
647 bl_idname = "btool.find_brush"
648 bl_label = ""
649 bl_description = "Find the selected brush"
651 obj: StringProperty("")
653 @classmethod
654 def poll(cls, context):
655 return context.active_object is not None
657 def execute(self, context):
658 for ob in bpy.context.view_layer.objects:
659 if ob.name == self.obj:
660 bpy.ops.object.select_all(action="TOGGLE")
661 bpy.ops.object.select_all(action="DESELECT")
662 bpy.context.view_layer.objects.active = ob
663 ob.select_set(state=True)
664 return {"FINISHED"}
667 # Move The Modifier in The Stack Up or Down
668 class BTool_MoveStack(Operator):
669 bl_idname = "btool.move_stack"
670 bl_label = ""
671 bl_description = "Move this Brush Up/Down in the Stack"
673 modif: StringProperty("")
674 direction: StringProperty("")
676 @classmethod
677 def poll(cls, context):
678 return context.active_object is not None
680 def execute(self, context):
681 if self.direction == "UP":
682 bpy.ops.object.modifier_move_up(modifier=self.modif)
683 if self.direction == "DOWN":
684 bpy.ops.object.modifier_move_down(modifier=self.modif)
685 return {"FINISHED"}
688 # Enable or Disable a Brush in the Three View
689 class BTool_EnableBrush(Operator):
690 bl_idname = "btool.enable_brush"
691 bl_label = ""
692 bl_description = "Removes all BoolTool config assigned to it"
694 thisObj: StringProperty("")
696 @classmethod
697 def poll(cls, context):
698 return context.active_object is not None
700 def execute(self, context):
701 # in this case is just one object but the function accept more than one at once
702 EnableBrush(context, [self.thisObj], context.active_object)
703 return {"FINISHED"}
706 # Enable or Disable a Brush Directly
707 class BTool_EnableThisBrush(Operator):
708 bl_idname = "btool.enable_this_brush"
709 bl_label = ""
710 bl_description = "Toggles this brush"
712 @classmethod
713 def poll(cls, context):
714 return context.active_object is not None
716 def execute(self, context):
717 EnableThisBrush(context, "None")
718 return {"FINISHED"}
721 # Enable or Disable a Brush Directly
722 class BTool_EnableFTransform(Operator):
723 bl_idname = "btool.enable_ftransf"
724 bl_label = ""
725 bl_description = "Use Fast Transformations to improve speed"
727 @classmethod
728 def poll(cls, context):
729 return context.active_object is not None
731 def execute(self, context):
732 EnableFTransf(context)
733 return {"FINISHED"}
736 # Other Operations -------------------------------------------------------
738 # Remove a Brush or a Canvas
739 class BTool_Remove(Operator):
740 bl_idname = "btool.remove"
741 bl_label = "Bool Tool Remove"
742 bl_description = "Removes all BoolTool config assigned to it"
743 bl_options = {"UNDO"}
745 thisObj: StringProperty("")
746 Prop: StringProperty("")
748 @classmethod
749 def poll(cls, context):
750 return context.active_object is not None
752 def execute(self, context):
753 Remove(context, self.thisObj, self.Prop)
754 return {"FINISHED"}
757 # Apply All to Canvas
758 class BTool_AllBrushToMesh(Operator):
759 bl_idname = "btool.to_mesh"
760 bl_label = "Apply All Canvas"
761 bl_description = "Apply all brushes of this canvas"
762 bl_options = {"UNDO"}
764 @classmethod
765 def poll(cls, context):
766 return context.active_object is not None
768 def execute(self, context):
769 lists = bpy.context.selected_objects
770 ApplyAll(context, lists)
771 return {"FINISHED"}
774 # Apply This Brush to the Canvas
775 class BTool_BrushToMesh(Operator):
776 bl_idname = "btool.brush_to_mesh"
777 bl_label = "Apply this Brush to Canvas"
778 bl_description = "Apply this brush to the canvas"
779 bl_options = {"UNDO"}
781 @classmethod
782 def poll(cls, context):
784 if isBrush(context.active_object):
785 return True
786 else:
787 return False
789 def execute(self, context):
790 ApplyThisBrush(context, bpy.context.active_object)
791 return {"FINISHED"}
794 # TODO
795 # Apply This Brush To Mesh
798 # ------------------- MENU CLASSES ------------------------------
800 # 3Dview Header Menu
801 class VIEW3D_MT_booltool_menu(Menu):
802 bl_label = "Bool Tool"
803 bl_idname = "VIEW3D_MT_booltool_menu"
805 def draw(self, context):
806 layout = self.layout
808 layout.label(text="Auto Boolean")
809 layout.operator(OBJECT_OT_BoolTool_Auto_Difference.bl_idname, text="Difference", icon="SELECT_SUBTRACT")
810 layout.operator(OBJECT_OT_BoolTool_Auto_Union.bl_idname, text="Union", icon="SELECT_EXTEND")
811 layout.operator(OBJECT_OT_BoolTool_Auto_Intersect.bl_idname, text="Intersect", icon="SELECT_INTERSECT")
812 layout.operator(OBJECT_OT_BoolTool_Auto_Slice.bl_idname, text="Slice", icon="SELECT_DIFFERENCE")
814 layout.separator()
816 layout.label(text="Brush Boolean")
817 layout.operator(BTool_Diff.bl_idname, text="Difference", icon="SELECT_SUBTRACT")
818 layout.operator(BTool_Union.bl_idname, text="Union", icon="SELECT_EXTEND")
819 layout.operator(BTool_Inters.bl_idname, text="Intersect", icon="SELECT_INTERSECT")
820 layout.operator(BTool_Slice.bl_idname, text="Slice", icon="SELECT_DIFFERENCE")
822 if isCanvas(context.active_object):
823 layout.separator()
824 layout.operator(BTool_AllBrushToMesh.bl_idname, icon="MOD_LATTICE", text="Apply All")
825 Rem = layout.operator(BTool_Remove.bl_idname, icon="X", text="Remove All")
826 Rem.thisObj = ""
827 Rem.Prop = "CANVAS"
829 if isBrush(context.active_object):
830 layout.separator()
831 layout.operator(BTool_BrushToMesh.bl_idname, icon="MOD_LATTICE", text="Apply Brush")
832 Rem = layout.operator(BTool_Remove.bl_idname, icon="X", text="Remove Brush")
833 Rem.thisObj = ""
834 Rem.Prop = "BRUSH"
837 def VIEW3D_BoolTool_Menu(self, context):
838 self.layout.menu(VIEW3D_MT_booltool_menu.bl_idname)
841 # ---------------- Toolshelf: Tools ---------------------
844 class VIEW3D_PT_booltool_tools(Panel):
845 bl_category = "objectmode"
846 bl_label = "Bool Tool"
847 bl_space_type = "VIEW_3D"
848 bl_region_type = "UI"
849 bl_context = "objectmode"
850 bl_options = {'DEFAULT_CLOSED'}
852 @classmethod
853 def poll(cls, context):
854 return context.active_object is not None
856 def draw(self, context):
857 layout = self.layout
859 col = layout.column(align=True)
860 col.label(text="Auto Boolean")
861 col.operator(OBJECT_OT_BoolTool_Auto_Difference.bl_idname, text="Difference", icon="SELECT_SUBTRACT")
862 col.operator(OBJECT_OT_BoolTool_Auto_Union.bl_idname, text="Union", icon="SELECT_EXTEND")
863 col.operator(OBJECT_OT_BoolTool_Auto_Intersect.bl_idname, text="Intersect", icon="SELECT_INTERSECT")
864 col.operator(OBJECT_OT_BoolTool_Auto_Slice.bl_idname, text="Slice", icon="SELECT_DIFFERENCE")
866 col = layout.column(align=True)
867 col.label(text="Brush Boolean")
868 col.operator(BTool_Diff.bl_idname, text="Difference", icon="SELECT_SUBTRACT")
869 col.operator(BTool_Union.bl_idname, text="Union", icon="SELECT_EXTEND")
870 col.operator(BTool_Inters.bl_idname, text="Intersect", icon="SELECT_INTERSECT")
871 col.operator(BTool_Slice.bl_idname, text="Slice", icon="SELECT_DIFFERENCE")
873 # TODO Draw Poly Brush
874 # main.separator()
876 # col = main.column(align=True)
877 # col.label(text="Draw:", icon="MESH_CUBE")
878 # col.separator()
879 # col.operator(BTool_DrawPolyBrush.bl_idname, icon="LINE_DATA")
882 # ---------- Toolshelf: Properties --------------------------------------------------------
885 class VIEW3D_PT_booltool_config(Panel):
886 bl_category = "objectmode"
887 bl_label = "Properties"
888 bl_space_type = "VIEW_3D"
889 bl_region_type = "UI"
890 bl_context = "objectmode"
891 bl_parent_id = "VIEW3D_PT_booltool_tools"
893 @classmethod
894 def poll(cls, context):
895 actObj = context.active_object
896 return isCanvas(actObj) or isBrush(actObj) # or isPolyBrush(actObj)
898 def draw(self, context):
899 layout = self.layout
900 actObj = context.active_object
902 row = layout.row(align=True)
904 if isCanvas(actObj):
906 row.label(text="CANVAS", icon="MESH_GRID")
907 row = layout.row()
908 row.prop(context.scene, "BoolHide", text="Hide Bool objects")
909 row = layout.row(align=True)
910 row.operator(BTool_AllBrushToMesh.bl_idname, icon="MOD_LATTICE", text="Apply All")
912 row = layout.row(align=True)
913 Rem = row.operator(BTool_Remove.bl_idname, icon="X", text="Remove All")
914 Rem.thisObj = ""
915 Rem.Prop = "CANVAS"
917 if isBrush(actObj):
918 layout.separator()
920 if isBrush(actObj):
922 if actObj["BoolToolBrush"] == "DIFFERENCE":
923 icon = "SELECT_SUBTRACT"
924 elif actObj["BoolToolBrush"] == "UNION":
925 icon = "SELECT_EXTEND"
926 elif actObj["BoolToolBrush"] == "INTERSECT":
927 icon = "SELECT_INTERSECT"
928 elif actObj["BoolToolBrush"] == "SLICE":
929 icon = "SELECT_DIFFERENCE"
931 row.label(text="BRUSH", icon=icon)
933 if actObj["BoolTool_FTransform"] == "True":
934 icon = "PMARKER_ACT"
935 else:
936 icon = "PMARKER"
937 if isFTransf():
938 pass
940 if isFTransf():
941 row = layout.row(align=True)
942 row.operator(BTool_EnableFTransform.bl_idname, text="Fast Vis", icon=icon)
943 row.operator(BTool_EnableThisBrush.bl_idname, text="Enable", icon="HIDE_OFF")
944 else:
945 row.operator(BTool_EnableThisBrush.bl_idname, icon="HIDE_OFF")
947 layout.operator(BTool_BrushToMesh.bl_idname, icon="MOD_LATTICE", text="Apply Brush")
948 Rem = layout.operator(BTool_Remove.bl_idname, icon="X", text="Remove Brush")
949 Rem.thisObj = ""
950 Rem.Prop = "BRUSH"
952 # TODO
953 # if isPolyBrush(actObj):
954 # layout.label(text="POLY BRUSH", icon="LINE_DATA")
955 # mod = actObj.modifiers["BTool_PolyBrush"]
956 # layout.prop(mod, "thickness", text="Size")
959 # ---------- Toolshelf: Brush Viewer -------------------------------------------------------
962 class VIEW3D_PT_booltool_bviewer(Panel):
963 bl_category = "objectmode"
964 bl_label = "Brush Viewer"
965 bl_space_type = "VIEW_3D"
966 bl_region_type = "UI"
967 bl_context = "objectmode"
968 bl_parent_id = "VIEW3D_PT_booltool_tools"
970 @classmethod
971 def poll(cls, context):
972 actObj = bpy.context.active_object
974 if isCanvas(actObj):
975 return True
976 else:
977 return False
979 def draw(self, context):
981 actObj = bpy.context.active_object
983 if isCanvas(actObj):
985 for mod in actObj.modifiers:
986 container = self.layout.box()
987 row = container.row(align=True)
989 if "BTool_" in mod.name:
991 if mod.operation == "DIFFERENCE":
992 icon = "SELECT_SUBTRACT"
993 elif mod.operation == "UNION":
994 icon = "SELECT_EXTEND"
995 elif mod.operation == "INTERSECT":
996 icon = "SELECT_INTERSECT"
997 elif mod.operation == "SLICE":
998 icon = "SELECT_DIFFERENCE"
1000 objSelect = row.operator("btool.find_brush", text=mod.object.name, icon=icon, emboss=False)
1001 objSelect.obj = mod.object.name
1003 EnableIcon = "RESTRICT_VIEW_ON"
1004 if mod.show_viewport:
1005 EnableIcon = "RESTRICT_VIEW_OFF"
1006 Enable = row.operator(BTool_EnableBrush.bl_idname, icon=EnableIcon, emboss=False)
1007 Enable.thisObj = mod.object.name
1009 Remove = row.operator("btool.remove", text="", icon="X", emboss=False)
1010 Remove.thisObj = mod.object.name
1011 Remove.Prop = "THIS"
1013 else:
1014 row.label(text=mod.name)
1016 Up = row.operator("btool.move_stack", icon="TRIA_UP", emboss=False)
1017 Up.modif = mod.name
1018 Up.direction = "UP"
1020 Dw = row.operator("btool.move_stack", icon="TRIA_DOWN", emboss=False)
1021 Dw.modif = mod.name
1022 Dw.direction = "DOWN"
1025 # ------------------ BOOL TOOL ADD-ON PREFERENCES ----------------------------
1028 shortcut_list = (
1029 ("3D View", None),
1030 ("Menu", "Ctrl Shift B"),
1032 ("Auto Operators", None),
1033 ("Difference", "Ctrl Shift Num -"),
1034 ("Union", "Ctrl Shift Num +"),
1035 ("Intersect", "Ctrl Shift Num *"),
1036 ("Slice", "Ctrl Shift Num /"),
1038 ("Brush Operators", None),
1039 ("Difference", "Ctrl Num -"),
1040 ("Union", "Ctrl Num +"),
1041 ("Intersect", "Ctrl Num *"),
1042 ("Slice", "Ctrl Num /"),
1043 ("Brush To Mesh", "Ctrl Num Enter"),
1044 ("All Brushes To Mesh", "Ctrl Shift Num Enter"),
1048 def UpdateBoolTool_Pref(self, context):
1049 if self.fast_transform:
1050 RegisterFastT()
1051 else:
1052 UnRegisterFastT()
1055 # Define Panel classes for updating
1056 panels = (
1057 VIEW3D_PT_booltool_tools,
1058 VIEW3D_PT_booltool_config,
1059 VIEW3D_PT_booltool_bviewer,
1063 def update_panels(self, context):
1064 try:
1065 for panel in panels:
1066 if "bl_rna" in panel.__dict__:
1067 bpy.utils.unregister_class(panel)
1069 for panel in panels:
1070 panel.bl_category = context.preferences.addons[
1071 __name__
1072 ].preferences.category
1073 bpy.utils.register_class(panel)
1075 except Exception as e:
1076 message = "Bool Tool: Updating Panel locations has failed"
1077 print("\n[{}]\n{}\n\nError:\n{}".format(__name__, message, e))
1080 def icon_tria(prop):
1081 if prop:
1082 return "TRIA_DOWN"
1083 return "TRIA_RIGHT"
1086 class PREFS_BoolTool_Props(AddonPreferences):
1087 bl_idname = __name__
1089 fast_transform: BoolProperty(
1090 name="Fast Transformations",
1091 update=UpdateBoolTool_Pref,
1092 description="Replace the Transform HotKeys (G,R,S)\n"
1093 "for a custom version that can optimize the visualization of Brushes",
1095 use_wire: BoolProperty(
1096 name="Display As Wireframe",
1097 description="Display brush as wireframe instead of bounding box",
1099 category: StringProperty(
1100 name="Tab Name",
1101 description="Set sidebar tab name",
1102 default="Edit",
1103 update=update_panels,
1105 show_shortcuts: BoolProperty(name="Shortcuts")
1107 def draw(self, context):
1108 layout = self.layout
1109 layout.use_property_split = True
1110 layout.use_property_decorate = False
1112 col = layout.column()
1113 col.prop(self, "category")
1114 col.prop(self, "fast_transform")
1115 col.prop(self, "use_wire")
1117 col = layout.column()
1118 col.scale_y = 1.2
1119 col.use_property_split = False
1120 col.prop(self, "show_shortcuts", icon=icon_tria(self.show_shortcuts))
1122 if self.show_shortcuts:
1124 col = layout.column()
1126 for key_name, key_comb in shortcut_list:
1127 if key_comb is None:
1128 col.separator()
1129 col.label(text=key_name)
1130 else:
1131 row = col.row(align=True)
1132 row.scale_y = 0.7
1133 row.box().label(text=key_name)
1134 row.box().label(text=key_comb)
1137 # ------------------- Class List ------------------------------------------------
1139 classes = (
1140 PREFS_BoolTool_Props,
1141 VIEW3D_MT_booltool_menu,
1142 VIEW3D_PT_booltool_tools,
1143 VIEW3D_PT_booltool_config,
1144 VIEW3D_PT_booltool_bviewer,
1145 OBJECT_OT_BoolTool_Auto_Union,
1146 OBJECT_OT_BoolTool_Auto_Difference,
1147 OBJECT_OT_BoolTool_Auto_Intersect,
1148 OBJECT_OT_BoolTool_Auto_Slice,
1149 BTool_Union,
1150 BTool_Diff,
1151 BTool_Inters,
1152 BTool_Slice,
1153 # TODO Draw Poly Brush
1154 # BTool_DrawPolyBrush,
1155 BTool_Remove,
1156 BTool_AllBrushToMesh,
1157 BTool_BrushToMesh,
1158 BTool_FindBrush,
1159 BTool_MoveStack,
1160 BTool_EnableBrush,
1161 BTool_EnableThisBrush,
1162 BTool_EnableFTransform,
1163 BTool_FastTransform,
1167 # ------------------- REGISTER ------------------------------------------------
1169 addon_keymaps = []
1170 addon_keymapsFastT = []
1173 # Fast Transform HotKeys Register
1174 def RegisterFastT():
1175 wm = bpy.context.window_manager
1176 km = wm.keyconfigs.addon.keymaps.new(name="Object Mode", space_type="EMPTY")
1178 kmi = km.keymap_items.new(BTool_FastTransform.bl_idname, "G", "PRESS")
1179 kmi.properties.operator = "Translate"
1180 addon_keymapsFastT.append((km, kmi))
1182 kmi = km.keymap_items.new(BTool_FastTransform.bl_idname, "R", "PRESS")
1183 kmi.properties.operator = "Rotate"
1184 addon_keymapsFastT.append((km, kmi))
1186 kmi = km.keymap_items.new(BTool_FastTransform.bl_idname, "S", "PRESS")
1187 kmi.properties.operator = "Scale"
1188 addon_keymapsFastT.append((km, kmi))
1191 # Fast Transform HotKeys UnRegister
1192 def UnRegisterFastT():
1193 wm = bpy.context.window_manager
1194 kc = wm.keyconfigs.addon
1195 if kc:
1196 for km, kmi in addon_keymapsFastT:
1197 km.keymap_items.remove(kmi)
1199 addon_keymapsFastT.clear()
1202 def register():
1203 for cls in classes:
1204 bpy.utils.register_class(cls)
1205 update_panels(None, bpy.context)
1207 # Scene variables
1208 bpy.types.Scene.BoolHide = BoolProperty(
1209 default=False,
1210 description="Hide boolean objects",
1211 update=update_BoolHide,
1213 bpy.types.VIEW3D_MT_object.append(VIEW3D_BoolTool_Menu)
1215 wm = bpy.context.window_manager
1216 kc = wm.keyconfigs.addon
1218 # create the boolean menu hotkey
1219 if kc is not None:
1220 km = kc.keymaps.new(name="Object Mode")
1222 kmi = km.keymap_items.new("wm.call_menu", "B", "PRESS", ctrl=True, shift=True)
1223 kmi.properties.name = "VIEW3D_MT_booltool_menu"
1224 addon_keymaps.append((km, kmi))
1226 # Brush Operators
1227 kmi = km.keymap_items.new(BTool_Union.bl_idname, "NUMPAD_PLUS", "PRESS", ctrl=True)
1228 addon_keymaps.append((km, kmi))
1229 kmi = km.keymap_items.new(BTool_Diff.bl_idname, "NUMPAD_MINUS", "PRESS", ctrl=True)
1230 addon_keymaps.append((km, kmi))
1231 kmi = km.keymap_items.new(BTool_Inters.bl_idname, "NUMPAD_ASTERIX", "PRESS", ctrl=True)
1232 addon_keymaps.append((km, kmi))
1233 kmi = km.keymap_items.new(BTool_Slice.bl_idname, "NUMPAD_SLASH", "PRESS", ctrl=True)
1234 addon_keymaps.append((km, kmi))
1235 kmi = km.keymap_items.new(BTool_BrushToMesh.bl_idname, "NUMPAD_ENTER", "PRESS", ctrl=True)
1236 addon_keymaps.append((km, kmi))
1237 kmi = km.keymap_items.new(
1238 BTool_AllBrushToMesh.bl_idname,
1239 "NUMPAD_ENTER",
1240 "PRESS",
1241 ctrl=True,
1242 shift=True,
1244 addon_keymaps.append((km, kmi))
1246 # Auto Operators
1247 kmi = km.keymap_items.new(
1248 OBJECT_OT_BoolTool_Auto_Union.bl_idname,
1249 "NUMPAD_PLUS",
1250 "PRESS",
1251 ctrl=True,
1252 shift=True,
1254 addon_keymaps.append((km, kmi))
1255 kmi = km.keymap_items.new(
1256 OBJECT_OT_BoolTool_Auto_Difference.bl_idname,
1257 "NUMPAD_MINUS",
1258 "PRESS",
1259 ctrl=True,
1260 shift=True,
1262 addon_keymaps.append((km, kmi))
1263 kmi = km.keymap_items.new(
1264 OBJECT_OT_BoolTool_Auto_Intersect.bl_idname,
1265 "NUMPAD_ASTERIX",
1266 "PRESS",
1267 ctrl=True,
1268 shift=True,
1270 addon_keymaps.append((km, kmi))
1271 kmi = km.keymap_items.new(
1272 OBJECT_OT_BoolTool_Auto_Slice.bl_idname,
1273 "NUMPAD_SLASH",
1274 "PRESS",
1275 ctrl=True,
1276 shift=True,
1278 addon_keymaps.append((km, kmi))
1281 def unregister():
1282 # Keymapping
1283 # remove keymaps when add-on is deactivated
1284 wm = bpy.context.window_manager
1285 kc = wm.keyconfigs.addon
1286 if kc is not None:
1287 for km, kmi in addon_keymaps:
1288 km.keymap_items.remove(kmi)
1290 addon_keymaps.clear()
1291 UnRegisterFastT()
1292 bpy.types.VIEW3D_MT_object.remove(VIEW3D_BoolTool_Menu)
1293 del bpy.types.Scene.BoolHide
1295 for cls in classes:
1296 bpy.utils.unregister_class(cls)
1299 if __name__ == "__main__":
1300 register()