1 # SPDX-FileCopyrightText: 2016-2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
7 "author": "Vitor Balbio, Mikhail Rachinskiy, TynkaTopi, Meta-Androcto, Simon Appelt",
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",
17 from bpy
.types
import (
23 from bpy
.props
import (
29 # ------------------- Bool Tool FUNCTIONS -------------------------
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
39 o
.hide_viewport
= hide_state
44 if _obj
["BoolToolRoot"]:
52 if _obj
["BoolToolBrush"]:
59 # def isPolyBrush(_obj):
61 # if _obj["BoolToolPolyBrush"]:
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
):
84 for ob
in bpy
.context
.view_layer
.objects
:
86 for mod
in ob
.modifiers
:
87 if "BTool_" in mod
.name
:
88 if obj
.name
in mod
.name
:
93 preferences
= bpy
.context
.preferences
94 addons
= preferences
.addons
95 addon_prefs
= addons
[__name__
].preferences
96 if addon_prefs
.fast_transform
:
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
:
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
125 selObj
.display_type
= "WIRE"
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
)
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"
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
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
)
182 RemoveThis(thisObj_name
)
184 # If the remove was called from the Properties:
186 # Remove the Brush Property
188 Canvas
= FindCanvas(actObj
)
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)
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
):
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
216 mod
.show_viewport
= True
217 mod
.show_render
= True
220 # Find the Canvas and Enable this Brush
221 def EnableThisBrush(context
, set):
223 for obj
in bpy
.context
.view_layer
.objects
:
224 if obj
!= bpy
.context
.active_object
:
226 for mod
in obj
.modifiers
:
227 if "BTool_" in mod
.name
:
228 if mod
.object == bpy
.context
.active_object
:
231 for mod
in canvas
.modifiers
:
232 if "BTool_" in mod
.name
:
233 if mod
.object == bpy
.context
.active_object
:
235 if mod
.show_viewport
:
236 mod
.show_viewport
= False
237 mod
.show_render
= False
239 mod
.show_viewport
= True
240 mod
.show_render
= True
243 mod
.show_viewport
= True
245 mod
.show_viewport
= False
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"
256 actObj
["BoolTool_FTransform"] = "True"
260 # Apply All Brushes to the Canvas
261 def ApplyAll(context
, 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)
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
:
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
:
285 bpy
.ops
.object.delete()
288 # Apply This Brush to the Canvas
289 def ApplyThisBrush(context
, brush
):
290 for obj
in context
.scene
.objects
:
292 for mod
in obj
.modifiers
:
293 if "BTool_" + brush
.name
in mod
.name
:
295 context
.view_layer
.objects
.active
= obj
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")
305 brush
.select_set(True)
306 # bpy.ops.object.delete()
309 # ------------------ Bool Tool OPERATORS --------------------------------------
312 # class BTool_DrawPolyBrush(Operator):
313 # bl_idname = "btool.draw_polybrush"
314 # bl_label = "Draw Poly Brush"
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"
321 # store_cont_draw = False
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
333 # self.store_cont_draw = continuous
334 # tool_settings.use_gpencil_continuous_drawing = True
336 # tool_settings.use_gpencil_continuous_drawing = self.store_cont_draw
338 # def modal(self, context, event):
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)
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"
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):
397 # self.set_cont_draw(context, start=True)
398 # context.window_manager.modal_handler_add(self)
399 # return {"RUNNING_MODAL"}
401 # self.report({"WARNING"}, "No active object, could not finish")
402 # return {"CANCELLED"}
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("")
415 def modal(self
, context
, event
):
417 actObj
= bpy
.context
.active_object
418 useWire
= bpy
.context
.preferences
.addons
[__name__
].preferences
.use_wire
421 if isBrush(actObj
) and actObj
["BoolTool_FTransform"] == "True":
422 EnableThisBrush(bpy
.context
, "False")
424 actObj
.display_type
= "WIRE"
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":
437 EnableThisBrush(bpy
.context
, "True")
438 actObj
.display_type
= "WIRE"
441 if event
.type in {"RIGHTMOUSE", "ESC"}:
443 EnableThisBrush(bpy
.context
, "True")
444 actObj
.display_type
= "WIRE"
447 return {"RUNNING_MODAL"}
449 def invoke(self
, context
, event
):
451 context
.window_manager
.modal_handler_add(self
)
452 return {"RUNNING_MODAL"}
454 self
.report({"WARNING"}, "No active object, could not finish")
458 # ------------------- Bool Tool OPERATOR CLASSES --------------------------------------------------------
462 # --------------------------------------------------------------------------------------
467 def execute(self
, context
):
468 Operation(context
, self
.mode
)
471 def invoke(self
, context
, event
):
472 if len(context
.selected_objects
) < 2:
473 self
.report({"ERROR"}, "At least two objects must be selected")
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"}
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"}
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"}
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"}
515 # Auto Boolean operators
516 # --------------------------------------------------------------------------------------
521 def objects_prepare(self
):
522 for ob
in bpy
.context
.selected_objects
:
523 if ob
.type != "MESH":
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")
548 self
.mesh_selection(ob
, "SELECT")
549 self
.boolean_mod(obj
, ob
, self
.mode
)
553 def boolean_mod(self
, obj
, ob
, mode
, ob_delete
=True):
554 md
= obj
.modifiers
.new("Auto Boolean", "BOOLEAN")
555 md
.show_viewport
= False
559 context_override
= {'object': obj
}
560 with bpy
.context
.temp_override(**context_override
):
561 bpy
.ops
.object.modifier_apply(modifier
=md
.name
)
564 bpy
.data
.objects
.remove(ob
)
566 def execute(self
, context
):
567 self
.objects_prepare()
568 self
.boolean_operation()
571 def invoke(self
, context
, event
):
572 if len(context
.selected_objects
) < 2:
573 self
.report({"ERROR"}, "At least two objects must be selected")
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"}
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"}
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"}
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
)
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
643 # Utils Class ---------------------------------------------------------------
645 # Find the Brush Selected in Three View
646 class BTool_FindBrush(Operator
):
647 bl_idname
= "btool.find_brush"
649 bl_description
= "Find the selected brush"
651 obj
: StringProperty("")
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)
667 # Move The Modifier in The Stack Up or Down
668 class BTool_MoveStack(Operator
):
669 bl_idname
= "btool.move_stack"
671 bl_description
= "Move this Brush Up/Down in the Stack"
673 modif
: StringProperty("")
674 direction
: StringProperty("")
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
)
688 # Enable or Disable a Brush in the Three View
689 class BTool_EnableBrush(Operator
):
690 bl_idname
= "btool.enable_brush"
692 bl_description
= "Removes all BoolTool config assigned to it"
694 thisObj
: StringProperty("")
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
)
706 # Enable or Disable a Brush Directly
707 class BTool_EnableThisBrush(Operator
):
708 bl_idname
= "btool.enable_this_brush"
710 bl_description
= "Toggles this brush"
713 def poll(cls
, context
):
714 return context
.active_object
is not None
716 def execute(self
, context
):
717 EnableThisBrush(context
, "None")
721 # Enable or Disable a Brush Directly
722 class BTool_EnableFTransform(Operator
):
723 bl_idname
= "btool.enable_ftransf"
725 bl_description
= "Use Fast Transformations to improve speed"
728 def poll(cls
, context
):
729 return context
.active_object
is not None
731 def execute(self
, context
):
732 EnableFTransf(context
)
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("")
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
)
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"}
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
)
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"}
782 def poll(cls
, context
):
784 if isBrush(context
.active_object
):
789 def execute(self
, context
):
790 ApplyThisBrush(context
, bpy
.context
.active_object
)
795 # Apply This Brush To Mesh
798 # ------------------- MENU CLASSES ------------------------------
801 class VIEW3D_MT_booltool_menu(Menu
):
802 bl_label
= "Bool Tool"
803 bl_idname
= "VIEW3D_MT_booltool_menu"
805 def draw(self
, context
):
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")
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
):
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")
829 if isBrush(context
.active_object
):
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")
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'}
853 def poll(cls
, context
):
854 return context
.active_object
is not None
856 def draw(self
, context
):
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
876 # col = main.column(align=True)
877 # col.label(text="Draw:", icon="MESH_CUBE")
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"
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
):
900 actObj
= context
.active_object
902 row
= layout
.row(align
=True)
906 row
.label(text
="CANVAS", icon
="MESH_GRID")
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")
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":
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")
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")
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"
971 def poll(cls
, context
):
972 actObj
= bpy
.context
.active_object
979 def draw(self
, context
):
981 actObj
= bpy
.context
.active_object
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"
1014 row
.label(text
=mod
.name
)
1016 Up
= row
.operator("btool.move_stack", icon
="TRIA_UP", emboss
=False)
1020 Dw
= row
.operator("btool.move_stack", icon
="TRIA_DOWN", emboss
=False)
1022 Dw
.direction
= "DOWN"
1025 # ------------------ BOOL TOOL ADD-ON PREFERENCES ----------------------------
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
:
1055 # Define Panel classes for updating
1057 VIEW3D_PT_booltool_tools
,
1058 VIEW3D_PT_booltool_config
,
1059 VIEW3D_PT_booltool_bviewer
,
1063 def update_panels(self
, context
):
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
[
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
):
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(
1101 description
="Set sidebar tab name",
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()
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:
1129 col
.label(text
=key_name
)
1131 row
= col
.row(align
=True)
1133 row
.box().label(text
=key_name
)
1134 row
.box().label(text
=key_comb
)
1137 # ------------------- Class List ------------------------------------------------
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
,
1153 # TODO Draw Poly Brush
1154 # BTool_DrawPolyBrush,
1156 BTool_AllBrushToMesh
,
1161 BTool_EnableThisBrush
,
1162 BTool_EnableFTransform
,
1163 BTool_FastTransform
,
1167 # ------------------- REGISTER ------------------------------------------------
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
1196 for km
, kmi
in addon_keymapsFastT
:
1197 km
.keymap_items
.remove(kmi
)
1199 addon_keymapsFastT
.clear()
1204 bpy
.utils
.register_class(cls
)
1205 update_panels(None, bpy
.context
)
1208 bpy
.types
.Scene
.BoolHide
= BoolProperty(
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
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
))
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
,
1244 addon_keymaps
.append((km
, kmi
))
1247 kmi
= km
.keymap_items
.new(
1248 OBJECT_OT_BoolTool_Auto_Union
.bl_idname
,
1254 addon_keymaps
.append((km
, kmi
))
1255 kmi
= km
.keymap_items
.new(
1256 OBJECT_OT_BoolTool_Auto_Difference
.bl_idname
,
1262 addon_keymaps
.append((km
, kmi
))
1263 kmi
= km
.keymap_items
.new(
1264 OBJECT_OT_BoolTool_Auto_Intersect
.bl_idname
,
1270 addon_keymaps
.append((km
, kmi
))
1271 kmi
= km
.keymap_items
.new(
1272 OBJECT_OT_BoolTool_Auto_Slice
.bl_idname
,
1278 addon_keymaps
.append((km
, kmi
))
1283 # remove keymaps when add-on is deactivated
1284 wm
= bpy
.context
.window_manager
1285 kc
= wm
.keyconfigs
.addon
1287 for km
, kmi
in addon_keymaps
:
1288 km
.keymap_items
.remove(kmi
)
1290 addon_keymaps
.clear()
1292 bpy
.types
.VIEW3D_MT_object
.remove(VIEW3D_BoolTool_Menu
)
1293 del bpy
.types
.Scene
.BoolHide
1296 bpy
.utils
.unregister_class(cls
)
1299 if __name__
== "__main__":