Sun Position: fix error in HDRI mode when no env tex is selected
[blender-addons.git] / object_boolean_tools.py
blob149433cadc8d156b2b208f53698516dabb57ea5a
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 bl_info = {
4 "name": "Bool Tool",
5 "author": "Vitor Balbio, Mikhail Rachinskiy, TynkaTopi, Meta-Androcto, Simon Appelt",
6 "version": (0, 4, 1),
7 "blender": (2, 80, 0),
8 "location": "View3D > Sidebar > Edit Tab",
9 "description": "Bool Tool Hotkey: Ctrl Shift B",
10 "doc_url": "{BLENDER_MANUAL_URL}/addons/object/bool_tools.html",
11 "category": "Object",
14 import bpy
15 from bpy.types import (
16 AddonPreferences,
17 Operator,
18 Panel,
19 Menu,
21 from bpy.props import (
22 BoolProperty,
23 StringProperty,
27 # ------------------- Bool Tool FUNCTIONS -------------------------
28 # Utils:
30 # Hide boolean objects
31 def update_BoolHide(self, context):
32 ao = context.view_layer.objects.active
33 objs = [i.object for i in ao.modifiers if i.type == "BOOLEAN"]
34 hide_state = context.scene.BoolHide
36 for o in objs:
37 o.hide_viewport = hide_state
40 def isCanvas(_obj):
41 try:
42 if _obj["BoolToolRoot"]:
43 return True
44 except:
45 return False
48 def isBrush(_obj):
49 try:
50 if _obj["BoolToolBrush"]:
51 return True
52 except:
53 return False
56 # TODO
57 # def isPolyBrush(_obj):
58 # try:
59 # if _obj["BoolToolPolyBrush"]:
60 # return True
61 # except:
62 # return False
65 def object_visibility_set(ob, value=False):
66 ob.visible_camera = value
67 ob.visible_diffuse = value
68 ob.visible_glossy = value
69 ob.visible_shadow = value
70 ob.visible_transmission = value
71 ob.visible_volume_scatter = value
74 def BT_ObjectByName(obj):
75 for ob in bpy.context.view_layer.objects:
76 if isCanvas(ob) or isBrush(ob):
77 if ob.name == obj:
78 return ob
81 def FindCanvas(obj):
82 for ob in bpy.context.view_layer.objects:
83 if isCanvas(ob):
84 for mod in ob.modifiers:
85 if "BTool_" in mod.name:
86 if obj.name in mod.name:
87 return ob
90 def isFTransf():
91 preferences = bpy.context.preferences
92 addons = preferences.addons
93 addon_prefs = addons[__name__].preferences
94 if addon_prefs.fast_transform:
95 return True
96 else:
97 return False
100 def ConvertToMesh(obj):
101 act = bpy.context.view_layer.objects.active
102 bpy.context.view_layer.objects.active = obj
103 bpy.ops.object.convert(target="MESH")
104 bpy.context.view_layer.objects.active = act
107 # Do the Union, Difference and Intersection Operations with a Brush
108 def Operation(context, _operation):
109 prefs = context.preferences.addons[__name__].preferences
110 useWire = prefs.use_wire
112 for selObj in context.selected_objects:
113 if (
114 selObj != context.active_object and
115 (selObj.type == "MESH" or selObj.type == "CURVE")
117 if selObj.type == "CURVE":
118 ConvertToMesh(selObj)
119 actObj = context.active_object
120 selObj.hide_render = True
122 if useWire:
123 selObj.display_type = "WIRE"
124 else:
125 selObj.display_type = "BOUNDS"
127 object_visibility_set(selObj, value=False)
129 if _operation == "SLICE":
130 # copies instance_collection property(empty), but group property is empty (users_group = None)
131 clone = actObj.copy()
132 context.collection.objects.link(clone)
134 space_data = context.space_data
135 is_local_view = bool(space_data.local_view)
137 if is_local_view:
138 clone.local_view_set(space_data, True)
140 sliceMod = clone.modifiers.new("BTool_" + selObj.name, "BOOLEAN") # add mod to clone obj
141 sliceMod.object = selObj
142 sliceMod.operation = "DIFFERENCE"
143 clone["BoolToolRoot"] = True
145 newMod = actObj.modifiers.new("BTool_" + selObj.name, "BOOLEAN")
146 newMod.object = selObj
148 if _operation == "SLICE":
149 newMod.operation = "INTERSECT"
150 else:
151 newMod.operation = _operation
153 actObj["BoolToolRoot"] = True
154 selObj["BoolToolBrush"] = _operation
155 selObj["BoolTool_FTransform"] = "False"
158 # Remove Objects form the BoolTool System
159 def Remove(context, thisObj_name, Prop):
160 # Find the Brush pointed in the Tree View and Restore it, active is the Canvas
161 actObj = context.active_object
163 # Restore the Brush
164 def RemoveThis(_thisObj_name):
165 for obj in bpy.context.view_layer.objects:
166 # if it's the brush object
167 if obj.name == _thisObj_name:
168 obj.display_type = "TEXTURED"
169 del obj["BoolToolBrush"]
170 del obj["BoolTool_FTransform"]
171 object_visibility_set(obj, value=True)
173 # Remove it from the Canvas
174 for mod in actObj.modifiers:
175 if "BTool_" in mod.name:
176 if _thisObj_name in mod.name:
177 actObj.modifiers.remove(mod)
179 if Prop == "THIS":
180 RemoveThis(thisObj_name)
182 # If the remove was called from the Properties:
183 else:
184 # Remove the Brush Property
185 if Prop == "BRUSH":
186 Canvas = FindCanvas(actObj)
188 if Canvas:
189 for mod in Canvas.modifiers:
190 if "BTool_" in mod.name and actObj.name in mod.name:
191 Canvas.modifiers.remove(mod)
193 actObj.display_type = "TEXTURED"
194 del actObj["BoolToolBrush"]
195 del actObj["BoolTool_FTransform"]
196 object_visibility_set(actObj, value=True)
198 if Prop == "CANVAS":
199 for mod in actObj.modifiers:
200 if "BTool_" in mod.name:
201 RemoveThis(mod.object.name)
204 # Toggle the Enable the Brush Object Property
205 def EnableBrush(context, objList, canvas):
206 for obj in objList:
207 for mod in canvas.modifiers:
208 if "BTool_" in mod.name and mod.object.name == obj:
210 if mod.show_viewport:
211 mod.show_viewport = False
212 mod.show_render = False
213 else:
214 mod.show_viewport = True
215 mod.show_render = True
218 # Find the Canvas and Enable this Brush
219 def EnableThisBrush(context, set):
220 canvas = None
221 for obj in bpy.context.view_layer.objects:
222 if obj != bpy.context.active_object:
223 if isCanvas(obj):
224 for mod in obj.modifiers:
225 if "BTool_" in mod.name:
226 if mod.object == bpy.context.active_object:
227 canvas = obj
229 for mod in canvas.modifiers:
230 if "BTool_" in mod.name:
231 if mod.object == bpy.context.active_object:
232 if set == "None":
233 if mod.show_viewport:
234 mod.show_viewport = False
235 mod.show_render = False
236 else:
237 mod.show_viewport = True
238 mod.show_render = True
239 else:
240 if set == "True":
241 mod.show_viewport = True
242 else:
243 mod.show_viewport = False
244 return
247 # Toggle the Fast Transform Property of the Active Brush
248 def EnableFTransf(context):
249 actObj = bpy.context.active_object
251 if actObj["BoolTool_FTransform"] == "True":
252 actObj["BoolTool_FTransform"] = "False"
253 else:
254 actObj["BoolTool_FTransform"] = "True"
255 return
258 # Apply All Brushes to the Canvas
259 def ApplyAll(context, list):
260 objDeleteList = []
261 for selObj in list:
262 if isCanvas(selObj) and selObj == context.active_object:
263 for mod in selObj.modifiers:
264 if "BTool_" in mod.name:
265 objDeleteList.append(mod.object)
266 try:
267 bpy.ops.object.modifier_apply(modifier=mod.name)
268 except: # if fails the means it is multiuser data
269 context.active_object.data = context.active_object.data.copy() # so just make data unique
270 bpy.ops.object.modifier_apply(modifier=mod.name)
271 del selObj["BoolToolRoot"]
273 for obj in context.scene.objects:
274 if isCanvas(obj):
275 for mod in obj.modifiers:
276 # do not delete brush that is used by another canvas
277 if mod.type == "BOOLEAN" and mod.object in objDeleteList:
278 objDeleteList.remove(mod.object) # remove it from deletion
280 bpy.ops.object.select_all(action="DESELECT")
281 for obj in objDeleteList:
282 obj.select_set(True)
283 bpy.ops.object.delete()
286 # Apply This Brush to the Canvas
287 def ApplyThisBrush(context, brush):
288 for obj in context.scene.objects:
289 if isCanvas(obj):
290 for mod in obj.modifiers:
291 if "BTool_" + brush.name in mod.name:
292 # Apply This Brush
293 context.view_layer.objects.active = obj
294 try:
295 bpy.ops.object.modifier_apply(modifier=mod.name)
296 except: # if fails the means it is multiuser data
297 context.active_object.data = context.active_object.data.copy() # so just make data unique
298 bpy.ops.object.modifier_apply(modifier=mod.name)
299 bpy.ops.object.select_all(action="TOGGLE")
300 bpy.ops.object.select_all(action="DESELECT")
302 # Garbage Collector
303 brush.select_set(True)
304 # bpy.ops.object.delete()
307 # ------------------ Bool Tool OPERATORS --------------------------------------
309 # TODO
310 # class BTool_DrawPolyBrush(Operator):
311 # bl_idname = "btool.draw_polybrush"
312 # bl_label = "Draw Poly Brush"
313 # bl_description = (
314 # "Draw Polygonal Mask, can be applied to Canvas > Brush or Directly\n"
315 # "Note: ESC to Cancel, Enter to Apply, Right Click to erase the Lines"
318 # count = 0
319 # store_cont_draw = False
321 # @classmethod
322 # def poll(cls, context):
323 # return context.active_object is not None
325 # def set_cont_draw(self, context, start=False):
326 # # store / restore GP continuous drawing (see T52321)
327 # scene = context.scene
328 # tool_settings = scene.tool_settings
329 # continuous = tool_settings.use_gpencil_continuous_drawing
330 # if start:
331 # self.store_cont_draw = continuous
332 # tool_settings.use_gpencil_continuous_drawing = True
333 # else:
334 # tool_settings.use_gpencil_continuous_drawing = self.store_cont_draw
336 # def modal(self, context, event):
337 # self.count += 1
338 # actObj = bpy.context.active_object
339 # if self.count == 1:
340 # actObj.select_set(True)
341 # bpy.ops.gpencil.draw("INVOKE_DEFAULT", mode="DRAW_POLY")
343 # if event.type == "RIGHTMOUSE":
344 # # use this to pass to the Grease Pencil eraser (see T52321)
345 # pass
347 # if event.type in {"RET", "NUMPAD_ENTER"}:
349 # bpy.ops.gpencil.convert(type="POLY")
350 # self.set_cont_draw(context)
352 # for obj in context.selected_objects:
353 # if obj.type == "CURVE":
354 # obj.name = "PolyDraw"
355 # bpy.context.view_layer.objects.active = obj
356 # bpy.ops.object.select_all(action="DESELECT")
357 # obj.select_set(True)
358 # bpy.ops.object.convert(target="MESH")
359 # bpy.ops.object.mode_set(mode="EDIT")
360 # bpy.ops.mesh.select_all(action="SELECT")
361 # bpy.ops.mesh.edge_face_add()
362 # bpy.ops.mesh.flip_normals()
363 # bpy.ops.object.mode_set(mode="OBJECT")
364 # bpy.ops.object.origin_set(type="ORIGIN_CENTER_OF_MASS")
365 # bpy.ops.object.modifier_add(type="SOLIDIFY")
366 # for mod in obj.modifiers:
367 # if mod.name == "Solidify":
368 # mod.name = "BTool_PolyBrush"
369 # mod.thickness = 1
370 # mod.offset = 0
371 # obj["BoolToolPolyBrush"] = True
373 # bpy.ops.object.select_all(action="DESELECT")
374 # bpy.context.view_layer.objects.active = actObj
375 # bpy.context.view_layer.update()
376 # actObj.select_set(True)
377 # obj.select_set(True)
379 # bpy.context.view_layer.grease_pencil.clear()
380 # bpy.ops.gpencil.data_unlink()
382 # return {"FINISHED"}
384 # if event.type == "ESC":
385 # bpy.ops.ed.undo() # remove o Grease Pencil
386 # self.set_cont_draw(context)
388 # self.report({"INFO"}, "Draw Poly Brush: Operation Cancelled by User")
389 # return {"CANCELLED"}
391 # return {"RUNNING_MODAL"}
393 # def invoke(self, context, event):
394 # if context.object:
395 # self.set_cont_draw(context, start=True)
396 # context.window_manager.modal_handler_add(self)
397 # return {"RUNNING_MODAL"}
398 # else:
399 # self.report({"WARNING"}, "No active object, could not finish")
400 # return {"CANCELLED"}
403 # Fast Transform
404 class BTool_FastTransform(Operator):
405 bl_idname = "btool.fast_transform"
406 bl_label = "Fast Transform"
407 bl_description = "Enable Fast Transform"
409 operator: StringProperty("")
411 count = 0
413 def modal(self, context, event):
414 self.count += 1
415 actObj = bpy.context.active_object
416 useWire = bpy.context.preferences.addons[__name__].preferences.use_wire
417 if self.count == 1:
419 if isBrush(actObj) and actObj["BoolTool_FTransform"] == "True":
420 EnableThisBrush(bpy.context, "False")
421 if useWire:
422 actObj.display_type = "WIRE"
423 else:
424 actObj.display_type = "BOUNDS"
426 if self.operator == "Translate":
427 bpy.ops.transform.translate("INVOKE_DEFAULT")
428 if self.operator == "Rotate":
429 bpy.ops.transform.rotate("INVOKE_DEFAULT")
430 if self.operator == "Scale":
431 bpy.ops.transform.resize("INVOKE_DEFAULT")
433 if event.type == "LEFTMOUSE":
434 if isBrush(actObj):
435 EnableThisBrush(bpy.context, "True")
436 actObj.display_type = "WIRE"
437 return {"FINISHED"}
439 if event.type in {"RIGHTMOUSE", "ESC"}:
440 if isBrush(actObj):
441 EnableThisBrush(bpy.context, "True")
442 actObj.display_type = "WIRE"
443 return {"CANCELLED"}
445 return {"RUNNING_MODAL"}
447 def invoke(self, context, event):
448 if context.object:
449 context.window_manager.modal_handler_add(self)
450 return {"RUNNING_MODAL"}
451 else:
452 self.report({"WARNING"}, "No active object, could not finish")
453 return {"CANCELLED"}
456 # ------------------- Bool Tool OPERATOR CLASSES --------------------------------------------------------
459 # Brush operators
460 # --------------------------------------------------------------------------------------
463 class BToolSetup():
465 def execute(self, context):
466 Operation(context, self.mode)
467 return {"FINISHED"}
469 def invoke(self, context, event):
470 if len(context.selected_objects) < 2:
471 self.report({"ERROR"}, "At least two objects must be selected")
472 return {"CANCELLED"}
474 return self.execute(context)
477 class BTool_Union(Operator, BToolSetup):
478 bl_idname = "btool.boolean_union"
479 bl_label = "Brush Union"
480 bl_description = "This operator add a union brush to a canvas"
481 bl_options = {"REGISTER", "UNDO"}
483 mode = "UNION"
486 class BTool_Inters(Operator, BToolSetup):
487 bl_idname = "btool.boolean_inters"
488 bl_label = "Brush Intersection"
489 bl_description = "This operator add a intersect brush to a canvas"
490 bl_options = {"REGISTER", "UNDO"}
492 mode = "INTERSECT"
495 class BTool_Diff(Operator, BToolSetup):
496 bl_idname = "btool.boolean_diff"
497 bl_label = "Brush Difference"
498 bl_description = "This operator add a difference brush to a canvas"
499 bl_options = {"REGISTER", "UNDO"}
501 mode = "DIFFERENCE"
504 class BTool_Slice(Operator, BToolSetup):
505 bl_idname = "btool.boolean_slice"
506 bl_label = "Brush Slice"
507 bl_description = "This operator add a intersect brush to a canvas"
508 bl_options = {"REGISTER", "UNDO"}
510 mode = "SLICE"
513 # Auto Boolean operators
514 # --------------------------------------------------------------------------------------
517 class Auto_Boolean:
519 def objects_prepare(self):
520 for ob in bpy.context.selected_objects:
521 if ob.type != "MESH":
522 ob.select_set(False)
523 bpy.ops.object.make_single_user(object=True, obdata=True)
524 bpy.ops.object.convert(target="MESH")
526 def mesh_selection(self, ob, select_action):
527 obj = bpy.context.active_object
529 bpy.context.view_layer.objects.active = ob
530 bpy.ops.object.mode_set(mode="EDIT")
532 bpy.ops.mesh.reveal()
533 bpy.ops.mesh.select_all(action=select_action)
535 bpy.ops.object.mode_set(mode="OBJECT")
536 bpy.context.view_layer.objects.active = obj
538 def boolean_operation(self):
539 obj = bpy.context.active_object
540 obj.select_set(False)
541 obs = bpy.context.selected_objects
543 self.mesh_selection(obj, "DESELECT")
545 for ob in obs:
546 self.mesh_selection(ob, "SELECT")
547 self.boolean_mod(obj, ob, self.mode)
549 obj.select_set(True)
551 def boolean_mod(self, obj, ob, mode, ob_delete=True):
552 md = obj.modifiers.new("Auto Boolean", "BOOLEAN")
553 md.show_viewport = False
554 md.operation = mode
555 md.object = ob
557 override = {"object": obj}
558 bpy.ops.object.modifier_apply(override, modifier=md.name)
560 if ob_delete:
561 bpy.data.objects.remove(ob)
563 def execute(self, context):
564 self.objects_prepare()
565 self.boolean_operation()
566 return {"FINISHED"}
568 def invoke(self, context, event):
569 if len(context.selected_objects) < 2:
570 self.report({"ERROR"}, "At least two objects must be selected")
571 return {"CANCELLED"}
573 return self.execute(context)
576 class OBJECT_OT_BoolTool_Auto_Union(Operator, Auto_Boolean):
577 bl_idname = "object.booltool_auto_union"
578 bl_label = "Bool Tool Union"
579 bl_description = "Combine selected objects"
580 bl_options = {"REGISTER", "UNDO"}
582 mode = "UNION"
585 class OBJECT_OT_BoolTool_Auto_Difference(Operator, Auto_Boolean):
586 bl_idname = "object.booltool_auto_difference"
587 bl_label = "Bool Tool Difference"
588 bl_description = "Subtract selected objects from active object"
589 bl_options = {"REGISTER", "UNDO"}
591 mode = "DIFFERENCE"
594 class OBJECT_OT_BoolTool_Auto_Intersect(Operator, Auto_Boolean):
595 bl_idname = "object.booltool_auto_intersect"
596 bl_label = "Bool Tool Intersect"
597 bl_description = "Keep only intersecting geometry"
598 bl_options = {"REGISTER", "UNDO"}
600 mode = "INTERSECT"
603 class OBJECT_OT_BoolTool_Auto_Slice(Operator, Auto_Boolean):
604 bl_idname = "object.booltool_auto_slice"
605 bl_label = "Bool Tool Slice"
606 bl_description = "Slice active object along the selected objects"
607 bl_options = {"REGISTER", "UNDO"}
609 def execute(self, context):
610 space_data = context.space_data
611 is_local_view = bool(space_data.local_view)
612 self.objects_prepare()
614 ob1 = context.active_object
615 ob1.select_set(False)
616 self.mesh_selection(ob1, "DESELECT")
618 for ob2 in context.selected_objects:
620 self.mesh_selection(ob2, "SELECT")
622 ob1_copy = ob1.copy()
623 ob1_copy.data = ob1.data.copy()
625 for coll in ob1.users_collection:
626 coll.objects.link(ob1_copy)
628 if is_local_view:
629 ob1_copy.local_view_set(space_data, True)
631 self.boolean_mod(ob1, ob2, "DIFFERENCE", ob_delete=False)
632 self.boolean_mod(ob1_copy, ob2, "INTERSECT")
633 ob1_copy.select_set(True)
635 context.view_layer.objects.active = ob1_copy
637 return {"FINISHED"}
640 # Utils Class ---------------------------------------------------------------
642 # Find the Brush Selected in Three View
643 class BTool_FindBrush(Operator):
644 bl_idname = "btool.find_brush"
645 bl_label = ""
646 bl_description = "Find the selected brush"
648 obj: StringProperty("")
650 @classmethod
651 def poll(cls, context):
652 return context.active_object is not None
654 def execute(self, context):
655 for ob in bpy.context.view_layer.objects:
656 if ob.name == self.obj:
657 bpy.ops.object.select_all(action="TOGGLE")
658 bpy.ops.object.select_all(action="DESELECT")
659 bpy.context.view_layer.objects.active = ob
660 ob.set_select(state=True)
661 return {"FINISHED"}
664 # Move The Modifier in The Stack Up or Down
665 class BTool_MoveStack(Operator):
666 bl_idname = "btool.move_stack"
667 bl_label = ""
668 bl_description = "Move this Brush Up/Down in the Stack"
670 modif: StringProperty("")
671 direction: StringProperty("")
673 @classmethod
674 def poll(cls, context):
675 return context.active_object is not None
677 def execute(self, context):
678 if self.direction == "UP":
679 bpy.ops.object.modifier_move_up(modifier=self.modif)
680 if self.direction == "DOWN":
681 bpy.ops.object.modifier_move_down(modifier=self.modif)
682 return {"FINISHED"}
685 # Enable or Disable a Brush in the Three View
686 class BTool_EnableBrush(Operator):
687 bl_idname = "btool.enable_brush"
688 bl_label = ""
689 bl_description = "Removes all BoolTool config assigned to it"
691 thisObj: StringProperty("")
693 @classmethod
694 def poll(cls, context):
695 return context.active_object is not None
697 def execute(self, context):
698 # in this case is just one object but the function accept more than one at once
699 EnableBrush(context, [self.thisObj], context.active_object)
700 return {"FINISHED"}
703 # Enable or Disable a Brush Directly
704 class BTool_EnableThisBrush(Operator):
705 bl_idname = "btool.enable_this_brush"
706 bl_label = ""
707 bl_description = "Toggles this brush"
709 @classmethod
710 def poll(cls, context):
711 return context.active_object is not None
713 def execute(self, context):
714 EnableThisBrush(context, "None")
715 return {"FINISHED"}
718 # Enable or Disable a Brush Directly
719 class BTool_EnableFTransform(Operator):
720 bl_idname = "btool.enable_ftransf"
721 bl_label = ""
722 bl_description = "Use Fast Transformations to improve speed"
724 @classmethod
725 def poll(cls, context):
726 return context.active_object is not None
728 def execute(self, context):
729 EnableFTransf(context)
730 return {"FINISHED"}
733 # Other Operations -------------------------------------------------------
735 # Remove a Brush or a Canvas
736 class BTool_Remove(Operator):
737 bl_idname = "btool.remove"
738 bl_label = "Bool Tool Remove"
739 bl_description = "Removes all BoolTool config assigned to it"
740 bl_options = {"UNDO"}
742 thisObj: StringProperty("")
743 Prop: StringProperty("")
745 @classmethod
746 def poll(cls, context):
747 return context.active_object is not None
749 def execute(self, context):
750 Remove(context, self.thisObj, self.Prop)
751 return {"FINISHED"}
754 # Apply All to Canvas
755 class BTool_AllBrushToMesh(Operator):
756 bl_idname = "btool.to_mesh"
757 bl_label = "Apply All Canvas"
758 bl_description = "Apply all brushes of this canvas"
759 bl_options = {"UNDO"}
761 @classmethod
762 def poll(cls, context):
763 return context.active_object is not None
765 def execute(self, context):
766 lists = bpy.context.selected_objects
767 ApplyAll(context, lists)
768 return {"FINISHED"}
771 # Apply This Brush to the Canvas
772 class BTool_BrushToMesh(Operator):
773 bl_idname = "btool.brush_to_mesh"
774 bl_label = "Apply this Brush to Canvas"
775 bl_description = "Apply this brush to the canvas"
776 bl_options = {"UNDO"}
778 @classmethod
779 def poll(cls, context):
781 if isBrush(context.active_object):
782 return True
783 else:
784 return False
786 def execute(self, context):
787 ApplyThisBrush(context, bpy.context.active_object)
788 return {"FINISHED"}
791 # TODO
792 # Apply This Brush To Mesh
795 # ------------------- MENU CLASSES ------------------------------
797 # 3Dview Header Menu
798 class VIEW3D_MT_booltool_menu(Menu):
799 bl_label = "Bool Tool"
800 bl_idname = "VIEW3D_MT_booltool_menu"
802 def draw(self, context):
803 layout = self.layout
805 layout.label(text="Auto Boolean")
806 layout.operator(OBJECT_OT_BoolTool_Auto_Difference.bl_idname, text="Difference", icon="SELECT_SUBTRACT")
807 layout.operator(OBJECT_OT_BoolTool_Auto_Union.bl_idname, text="Union", icon="SELECT_EXTEND")
808 layout.operator(OBJECT_OT_BoolTool_Auto_Intersect.bl_idname, text="Intersect", icon="SELECT_INTERSECT")
809 layout.operator(OBJECT_OT_BoolTool_Auto_Slice.bl_idname, text="Slice", icon="SELECT_DIFFERENCE")
811 layout.separator()
813 layout.label(text="Brush Boolean")
814 layout.operator(BTool_Diff.bl_idname, text="Difference", icon="SELECT_SUBTRACT")
815 layout.operator(BTool_Union.bl_idname, text="Union", icon="SELECT_EXTEND")
816 layout.operator(BTool_Inters.bl_idname, text="Intersect", icon="SELECT_INTERSECT")
817 layout.operator(BTool_Slice.bl_idname, text="Slice", icon="SELECT_DIFFERENCE")
819 if isCanvas(context.active_object):
820 layout.separator()
821 layout.operator(BTool_AllBrushToMesh.bl_idname, icon="MOD_LATTICE", text="Apply All")
822 Rem = layout.operator(BTool_Remove.bl_idname, icon="X", text="Remove All")
823 Rem.thisObj = ""
824 Rem.Prop = "CANVAS"
826 if isBrush(context.active_object):
827 layout.separator()
828 layout.operator(BTool_BrushToMesh.bl_idname, icon="MOD_LATTICE", text="Apply Brush")
829 Rem = layout.operator(BTool_Remove.bl_idname, icon="X", text="Remove Brush")
830 Rem.thisObj = ""
831 Rem.Prop = "BRUSH"
834 def VIEW3D_BoolTool_Menu(self, context):
835 self.layout.menu(VIEW3D_MT_booltool_menu.bl_idname)
838 # ---------------- Toolshelf: Tools ---------------------
841 class VIEW3D_PT_booltool_tools(Panel):
842 bl_category = "objectmode"
843 bl_label = "Bool Tool"
844 bl_space_type = "VIEW_3D"
845 bl_region_type = "UI"
846 bl_context = "objectmode"
847 bl_options = {'DEFAULT_CLOSED'}
849 @classmethod
850 def poll(cls, context):
851 return context.active_object is not None
853 def draw(self, context):
854 layout = self.layout
856 col = layout.column(align=True)
857 col.label(text="Auto Boolean")
858 col.operator(OBJECT_OT_BoolTool_Auto_Difference.bl_idname, text="Difference", icon="SELECT_SUBTRACT")
859 col.operator(OBJECT_OT_BoolTool_Auto_Union.bl_idname, text="Union", icon="SELECT_EXTEND")
860 col.operator(OBJECT_OT_BoolTool_Auto_Intersect.bl_idname, text="Intersect", icon="SELECT_INTERSECT")
861 col.operator(OBJECT_OT_BoolTool_Auto_Slice.bl_idname, text="Slice", icon="SELECT_DIFFERENCE")
863 col = layout.column(align=True)
864 col.label(text="Brush Boolean")
865 col.operator(BTool_Diff.bl_idname, text="Difference", icon="SELECT_SUBTRACT")
866 col.operator(BTool_Union.bl_idname, text="Union", icon="SELECT_EXTEND")
867 col.operator(BTool_Inters.bl_idname, text="Intersect", icon="SELECT_INTERSECT")
868 col.operator(BTool_Slice.bl_idname, text="Slice", icon="SELECT_DIFFERENCE")
870 # TODO Draw Poly Brush
871 # main.separator()
873 # col = main.column(align=True)
874 # col.label(text="Draw:", icon="MESH_CUBE")
875 # col.separator()
876 # col.operator(BTool_DrawPolyBrush.bl_idname, icon="LINE_DATA")
879 # ---------- Toolshelf: Properties --------------------------------------------------------
882 class VIEW3D_PT_booltool_config(Panel):
883 bl_category = "objectmode"
884 bl_label = "Properties"
885 bl_space_type = "VIEW_3D"
886 bl_region_type = "UI"
887 bl_context = "objectmode"
888 bl_parent_id = "VIEW3D_PT_booltool_tools"
890 @classmethod
891 def poll(cls, context):
892 actObj = context.active_object
893 return isCanvas(actObj) or isBrush(actObj) # or isPolyBrush(actObj)
895 def draw(self, context):
896 layout = self.layout
897 actObj = context.active_object
899 row = layout.row(align=True)
901 if isCanvas(actObj):
903 row.label(text="CANVAS", icon="MESH_GRID")
904 row = layout.row()
905 row.prop(context.scene, "BoolHide", text="Hide Bool objects")
906 row = layout.row(align=True)
907 row.operator(BTool_AllBrushToMesh.bl_idname, icon="MOD_LATTICE", text="Apply All")
909 row = layout.row(align=True)
910 Rem = row.operator(BTool_Remove.bl_idname, icon="X", text="Remove All")
911 Rem.thisObj = ""
912 Rem.Prop = "CANVAS"
914 if isBrush(actObj):
915 layout.separator()
917 if isBrush(actObj):
919 if actObj["BoolToolBrush"] == "DIFFERENCE":
920 icon = "SELECT_SUBTRACT"
921 elif actObj["BoolToolBrush"] == "UNION":
922 icon = "SELECT_EXTEND"
923 elif actObj["BoolToolBrush"] == "INTERSECT":
924 icon = "SELECT_INTERSECT"
925 elif actObj["BoolToolBrush"] == "SLICE":
926 icon = "SELECT_DIFFERENCE"
928 row.label(text="BRUSH", icon=icon)
930 if actObj["BoolTool_FTransform"] == "True":
931 icon = "PMARKER_ACT"
932 else:
933 icon = "PMARKER"
934 if isFTransf():
935 pass
937 if isFTransf():
938 row = layout.row(align=True)
939 row.operator(BTool_EnableFTransform.bl_idname, text="Fast Vis", icon=icon)
940 row.operator(BTool_EnableThisBrush.bl_idname, text="Enable", icon="HIDE_OFF")
941 else:
942 row.operator(BTool_EnableThisBrush.bl_idname, icon="HIDE_OFF")
944 layout.operator(BTool_BrushToMesh.bl_idname, icon="MOD_LATTICE", text="Apply Brush")
945 Rem = layout.operator(BTool_Remove.bl_idname, icon="X", text="Remove Brush")
946 Rem.thisObj = ""
947 Rem.Prop = "BRUSH"
949 # TODO
950 # if isPolyBrush(actObj):
951 # layout.label(text="POLY BRUSH", icon="LINE_DATA")
952 # mod = actObj.modifiers["BTool_PolyBrush"]
953 # layout.prop(mod, "thickness", text="Size")
956 # ---------- Toolshelf: Brush Viewer -------------------------------------------------------
959 class VIEW3D_PT_booltool_bviewer(Panel):
960 bl_category = "objectmode"
961 bl_label = "Brush Viewer"
962 bl_space_type = "VIEW_3D"
963 bl_region_type = "UI"
964 bl_context = "objectmode"
965 bl_parent_id = "VIEW3D_PT_booltool_tools"
967 @classmethod
968 def poll(cls, context):
969 actObj = bpy.context.active_object
971 if isCanvas(actObj):
972 return True
973 else:
974 return False
976 def draw(self, context):
978 actObj = bpy.context.active_object
980 if isCanvas(actObj):
982 for mod in actObj.modifiers:
983 container = self.layout.box()
984 row = container.row(align=True)
986 if "BTool_" in mod.name:
988 if mod.operation == "DIFFERENCE":
989 icon = "SELECT_SUBTRACT"
990 elif mod.operation == "UNION":
991 icon = "SELECT_EXTEND"
992 elif mod.operation == "INTERSECT":
993 icon = "SELECT_INTERSECT"
994 elif mod.operation == "SLICE":
995 icon = "SELECT_DIFFERENCE"
997 objSelect = row.operator("btool.find_brush", text=mod.object.name, icon=icon, emboss=False)
998 objSelect.obj = mod.object.name
1000 EnableIcon = "RESTRICT_VIEW_ON"
1001 if mod.show_viewport:
1002 EnableIcon = "RESTRICT_VIEW_OFF"
1003 Enable = row.operator(BTool_EnableBrush.bl_idname, icon=EnableIcon, emboss=False)
1004 Enable.thisObj = mod.object.name
1006 Remove = row.operator("btool.remove", text="", icon="X", emboss=False)
1007 Remove.thisObj = mod.object.name
1008 Remove.Prop = "THIS"
1010 else:
1011 row.label(text=mod.name)
1013 Up = row.operator("btool.move_stack", icon="TRIA_UP", emboss=False)
1014 Up.modif = mod.name
1015 Up.direction = "UP"
1017 Dw = row.operator("btool.move_stack", icon="TRIA_DOWN", emboss=False)
1018 Dw.modif = mod.name
1019 Dw.direction = "DOWN"
1022 # ------------------ BOOL TOOL ADD-ON PREFERENCES ----------------------------
1025 shortcut_list = (
1026 ("3D View", None),
1027 ("Menu", "Ctrl Shift B"),
1029 ("Auto Operators", None),
1030 ("Difference", "Ctrl Shift Num -"),
1031 ("Union", "Ctrl Shift Num +"),
1032 ("Intersect", "Ctrl Shift Num *"),
1033 ("Slice", "Ctrl Shift Num /"),
1035 ("Brush Operators", None),
1036 ("Difference", "Ctrl Num -"),
1037 ("Union", "Ctrl Num +"),
1038 ("Intersect", "Ctrl Num *"),
1039 ("Slice", "Ctrl Num /"),
1040 ("Brush To Mesh", "Ctrl Num Enter"),
1041 ("All Brushes To Mesh", "Ctrl Shift Num Enter"),
1045 def UpdateBoolTool_Pref(self, context):
1046 if self.fast_transform:
1047 RegisterFastT()
1048 else:
1049 UnRegisterFastT()
1052 # Define Panel classes for updating
1053 panels = (
1054 VIEW3D_PT_booltool_tools,
1055 VIEW3D_PT_booltool_config,
1056 VIEW3D_PT_booltool_bviewer,
1060 def update_panels(self, context):
1061 try:
1062 for panel in panels:
1063 if "bl_rna" in panel.__dict__:
1064 bpy.utils.unregister_class(panel)
1066 for panel in panels:
1067 panel.bl_category = context.preferences.addons[
1068 __name__
1069 ].preferences.category
1070 bpy.utils.register_class(panel)
1072 except Exception as e:
1073 message = "Bool Tool: Updating Panel locations has failed"
1074 print("\n[{}]\n{}\n\nError:\n{}".format(__name__, message, e))
1077 def icon_tria(prop):
1078 if prop:
1079 return "TRIA_DOWN"
1080 return "TRIA_RIGHT"
1083 class PREFS_BoolTool_Props(AddonPreferences):
1084 bl_idname = __name__
1086 fast_transform: BoolProperty(
1087 name="Fast Transformations",
1088 update=UpdateBoolTool_Pref,
1089 description="Replace the Transform HotKeys (G,R,S)\n"
1090 "for a custom version that can optimize the visualization of Brushes",
1092 use_wire: BoolProperty(
1093 name="Display As Wireframe",
1094 description="Display brush as wireframe instead of bounding box",
1096 category: StringProperty(
1097 name="Tab Name",
1098 description="Set sidebar tab name",
1099 default="Edit",
1100 update=update_panels,
1102 show_shortcuts: BoolProperty(name="Shortcuts")
1104 def draw(self, context):
1105 layout = self.layout
1106 layout.use_property_split = True
1107 layout.use_property_decorate = False
1109 col = layout.column()
1110 col.prop(self, "category")
1111 col.prop(self, "fast_transform")
1112 col.prop(self, "use_wire")
1114 col = layout.column()
1115 col.scale_y = 1.2
1116 col.use_property_split = False
1117 col.prop(self, "show_shortcuts", icon=icon_tria(self.show_shortcuts))
1119 if self.show_shortcuts:
1121 col = layout.column()
1123 for key_name, key_comb in shortcut_list:
1124 if key_comb is None:
1125 col.separator()
1126 col.label(text=key_name)
1127 else:
1128 row = col.row(align=True)
1129 row.scale_y = 0.7
1130 row.box().label(text=key_name)
1131 row.box().label(text=key_comb)
1134 # ------------------- Class List ------------------------------------------------
1136 classes = (
1137 PREFS_BoolTool_Props,
1138 VIEW3D_MT_booltool_menu,
1139 VIEW3D_PT_booltool_tools,
1140 VIEW3D_PT_booltool_config,
1141 VIEW3D_PT_booltool_bviewer,
1142 OBJECT_OT_BoolTool_Auto_Union,
1143 OBJECT_OT_BoolTool_Auto_Difference,
1144 OBJECT_OT_BoolTool_Auto_Intersect,
1145 OBJECT_OT_BoolTool_Auto_Slice,
1146 BTool_Union,
1147 BTool_Diff,
1148 BTool_Inters,
1149 BTool_Slice,
1150 # TODO Draw Poly Brush
1151 # BTool_DrawPolyBrush,
1152 BTool_Remove,
1153 BTool_AllBrushToMesh,
1154 BTool_BrushToMesh,
1155 BTool_FindBrush,
1156 BTool_MoveStack,
1157 BTool_EnableBrush,
1158 BTool_EnableThisBrush,
1159 BTool_EnableFTransform,
1160 BTool_FastTransform,
1164 # ------------------- REGISTER ------------------------------------------------
1166 addon_keymaps = []
1167 addon_keymapsFastT = []
1170 # Fast Transform HotKeys Register
1171 def RegisterFastT():
1172 wm = bpy.context.window_manager
1173 km = wm.keyconfigs.addon.keymaps.new(name="Object Mode", space_type="EMPTY")
1175 kmi = km.keymap_items.new(BTool_FastTransform.bl_idname, "G", "PRESS")
1176 kmi.properties.operator = "Translate"
1177 addon_keymapsFastT.append((km, kmi))
1179 kmi = km.keymap_items.new(BTool_FastTransform.bl_idname, "R", "PRESS")
1180 kmi.properties.operator = "Rotate"
1181 addon_keymapsFastT.append((km, kmi))
1183 kmi = km.keymap_items.new(BTool_FastTransform.bl_idname, "S", "PRESS")
1184 kmi.properties.operator = "Scale"
1185 addon_keymapsFastT.append((km, kmi))
1188 # Fast Transform HotKeys UnRegister
1189 def UnRegisterFastT():
1190 wm = bpy.context.window_manager
1191 kc = wm.keyconfigs.addon
1192 if kc:
1193 for km, kmi in addon_keymapsFastT:
1194 km.keymap_items.remove(kmi)
1196 addon_keymapsFastT.clear()
1199 def register():
1200 for cls in classes:
1201 bpy.utils.register_class(cls)
1202 update_panels(None, bpy.context)
1204 # Scene variables
1205 bpy.types.Scene.BoolHide = BoolProperty(
1206 default=False,
1207 description="Hide boolean objects",
1208 update=update_BoolHide,
1210 bpy.types.VIEW3D_MT_object.append(VIEW3D_BoolTool_Menu)
1212 wm = bpy.context.window_manager
1213 kc = wm.keyconfigs.addon
1215 # create the boolean menu hotkey
1216 if kc is not None:
1217 km = kc.keymaps.new(name="Object Mode")
1219 kmi = km.keymap_items.new("wm.call_menu", "B", "PRESS", ctrl=True, shift=True)
1220 kmi.properties.name = "VIEW3D_MT_booltool_menu"
1221 addon_keymaps.append((km, kmi))
1223 # Brush Operators
1224 kmi = km.keymap_items.new(BTool_Union.bl_idname, "NUMPAD_PLUS", "PRESS", ctrl=True)
1225 addon_keymaps.append((km, kmi))
1226 kmi = km.keymap_items.new(BTool_Diff.bl_idname, "NUMPAD_MINUS", "PRESS", ctrl=True)
1227 addon_keymaps.append((km, kmi))
1228 kmi = km.keymap_items.new(BTool_Inters.bl_idname, "NUMPAD_ASTERIX", "PRESS", ctrl=True)
1229 addon_keymaps.append((km, kmi))
1230 kmi = km.keymap_items.new(BTool_Slice.bl_idname, "NUMPAD_SLASH", "PRESS", ctrl=True)
1231 addon_keymaps.append((km, kmi))
1232 kmi = km.keymap_items.new(BTool_BrushToMesh.bl_idname, "NUMPAD_ENTER", "PRESS", ctrl=True)
1233 addon_keymaps.append((km, kmi))
1234 kmi = km.keymap_items.new(
1235 BTool_AllBrushToMesh.bl_idname,
1236 "NUMPAD_ENTER",
1237 "PRESS",
1238 ctrl=True,
1239 shift=True,
1241 addon_keymaps.append((km, kmi))
1243 # Auto Operators
1244 kmi = km.keymap_items.new(
1245 OBJECT_OT_BoolTool_Auto_Union.bl_idname,
1246 "NUMPAD_PLUS",
1247 "PRESS",
1248 ctrl=True,
1249 shift=True,
1251 addon_keymaps.append((km, kmi))
1252 kmi = km.keymap_items.new(
1253 OBJECT_OT_BoolTool_Auto_Difference.bl_idname,
1254 "NUMPAD_MINUS",
1255 "PRESS",
1256 ctrl=True,
1257 shift=True,
1259 addon_keymaps.append((km, kmi))
1260 kmi = km.keymap_items.new(
1261 OBJECT_OT_BoolTool_Auto_Intersect.bl_idname,
1262 "NUMPAD_ASTERIX",
1263 "PRESS",
1264 ctrl=True,
1265 shift=True,
1267 addon_keymaps.append((km, kmi))
1268 kmi = km.keymap_items.new(
1269 OBJECT_OT_BoolTool_Auto_Slice.bl_idname,
1270 "NUMPAD_SLASH",
1271 "PRESS",
1272 ctrl=True,
1273 shift=True,
1275 addon_keymaps.append((km, kmi))
1278 def unregister():
1279 # Keymapping
1280 # remove keymaps when add-on is deactivated
1281 wm = bpy.context.window_manager
1282 kc = wm.keyconfigs.addon
1283 if kc is not None:
1284 for km, kmi in addon_keymaps:
1285 km.keymap_items.remove(kmi)
1287 addon_keymaps.clear()
1288 UnRegisterFastT()
1289 bpy.types.VIEW3D_MT_object.remove(VIEW3D_BoolTool_Menu)
1290 del bpy.types.Scene.BoolHide
1292 for cls in classes:
1293 bpy.utils.unregister_class(cls)
1296 if __name__ == "__main__":
1297 register()