1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
23 "author": "Vitor Balbio, Mikhail Rachinskiy, TynkaTopi, Meta-Androcto, Simon Appelt",
25 "blender": (2, 80, 0),
26 "location": "View3D > Sidebar > Edit Tab",
27 "description": "Bool Tool Hotkey: Ctrl Shift B",
32 from bpy
.types
import (
38 from bpy
.props
import (
44 # ------------------- Bool Tool FUNCTIONS -------------------------
47 # Hide boolean objects
48 def update_BoolHide(self
, context
):
49 ao
= context
.view_layer
.objects
.active
50 objs
= [i
.object for i
in ao
.modifiers
if i
.type == "BOOLEAN"]
51 hide_state
= context
.scene
.BoolHide
54 o
.hide_viewport
= hide_state
59 if _obj
["BoolToolRoot"]:
67 if _obj
["BoolToolBrush"]:
74 # def isPolyBrush(_obj):
76 # if _obj["BoolToolPolyBrush"]:
82 def cycles_visibility_set(ob
, value
=False):
83 if not hasattr(ob
, "cycles_visibility"):
86 vis
= ob
.cycles_visibility
92 vis
.transmission
= value
96 def BT_ObjectByName(obj
):
97 for ob
in bpy
.context
.view_layer
.objects
:
98 if isCanvas(ob
) or isBrush(ob
):
104 for ob
in bpy
.context
.view_layer
.objects
:
106 for mod
in ob
.modifiers
:
107 if "BTool_" in mod
.name
:
108 if obj
.name
in mod
.name
:
113 preferences
= bpy
.context
.preferences
114 addons
= preferences
.addons
115 addon_prefs
= addons
[__name__
].preferences
116 if addon_prefs
.fast_transform
:
122 def ConvertToMesh(obj
):
123 act
= bpy
.context
.view_layer
.objects
.active
124 bpy
.context
.view_layer
.objects
.active
= obj
125 bpy
.ops
.object.convert(target
="MESH")
126 bpy
.context
.view_layer
.objects
.active
= act
129 # Do the Union, Difference and Intersection Operations with a Brush
130 def Operation(context
, _operation
):
131 prefs
= context
.preferences
.addons
[__name__
].preferences
132 useWire
= prefs
.use_wire
134 for selObj
in context
.selected_objects
:
136 selObj
!= context
.active_object
and
137 (selObj
.type == "MESH" or selObj
.type == "CURVE")
139 if selObj
.type == "CURVE":
140 ConvertToMesh(selObj
)
141 actObj
= context
.active_object
142 selObj
.hide_render
= True
145 selObj
.display_type
= "WIRE"
147 selObj
.display_type
= "BOUNDS"
149 cycles_visibility_set(selObj
, value
=False)
151 if _operation
== "SLICE":
152 # copies instance_collection property(empty), but group property is empty (users_group = None)
153 clone
= actObj
.copy()
154 context
.collection
.objects
.link(clone
)
156 space_data
= context
.space_data
157 is_local_view
= bool(space_data
.local_view
)
160 clone
.local_view_set(space_data
, True)
162 sliceMod
= clone
.modifiers
.new("BTool_" + selObj
.name
, "BOOLEAN") # add mod to clone obj
163 sliceMod
.object = selObj
164 sliceMod
.operation
= "DIFFERENCE"
165 clone
["BoolToolRoot"] = True
167 newMod
= actObj
.modifiers
.new("BTool_" + selObj
.name
, "BOOLEAN")
168 newMod
.object = selObj
170 if _operation
== "SLICE":
171 newMod
.operation
= "INTERSECT"
173 newMod
.operation
= _operation
175 actObj
["BoolToolRoot"] = True
176 selObj
["BoolToolBrush"] = _operation
177 selObj
["BoolTool_FTransform"] = "False"
180 # Remove Objects form the BoolTool System
181 def Remove(context
, thisObj_name
, Prop
):
182 # Find the Brush pointed in the Tree View and Restore it, active is the Canvas
183 actObj
= context
.active_object
186 def RemoveThis(_thisObj_name
):
187 for obj
in bpy
.context
.view_layer
.objects
:
188 # if it's the brush object
189 if obj
.name
== _thisObj_name
:
190 obj
.display_type
= "TEXTURED"
191 del obj
["BoolToolBrush"]
192 del obj
["BoolTool_FTransform"]
193 cycles_visibility_set(obj
, value
=True)
195 # Remove it from the Canvas
196 for mod
in actObj
.modifiers
:
197 if "BTool_" in mod
.name
:
198 if _thisObj_name
in mod
.name
:
199 actObj
.modifiers
.remove(mod
)
202 RemoveThis(thisObj_name
)
204 # If the remove was called from the Properties:
206 # Remove the Brush Property
208 Canvas
= FindCanvas(actObj
)
211 for mod
in Canvas
.modifiers
:
212 if "BTool_" in mod
.name
and actObj
.name
in mod
.name
:
213 Canvas
.modifiers
.remove(mod
)
215 actObj
.display_type
= "TEXTURED"
216 del actObj
["BoolToolBrush"]
217 del actObj
["BoolTool_FTransform"]
218 cycles_visibility_set(actObj
, value
=True)
221 for mod
in actObj
.modifiers
:
222 if "BTool_" in mod
.name
:
223 RemoveThis(mod
.object.name
)
226 # Toggle the Enable the Brush Object Property
227 def EnableBrush(context
, objList
, canvas
):
229 for mod
in canvas
.modifiers
:
230 if "BTool_" in mod
.name
and mod
.object.name
== obj
:
232 if mod
.show_viewport
:
233 mod
.show_viewport
= False
234 mod
.show_render
= False
236 mod
.show_viewport
= True
237 mod
.show_render
= True
240 # Find the Canvas and Enable this Brush
241 def EnableThisBrush(context
, set):
243 for obj
in bpy
.context
.view_layer
.objects
:
244 if obj
!= bpy
.context
.active_object
:
246 for mod
in obj
.modifiers
:
247 if "BTool_" in mod
.name
:
248 if mod
.object == bpy
.context
.active_object
:
251 for mod
in canvas
.modifiers
:
252 if "BTool_" in mod
.name
:
253 if mod
.object == bpy
.context
.active_object
:
255 if mod
.show_viewport
:
256 mod
.show_viewport
= False
257 mod
.show_render
= False
259 mod
.show_viewport
= True
260 mod
.show_render
= True
263 mod
.show_viewport
= True
265 mod
.show_viewport
= False
269 # Toggle the Fast Transform Property of the Active Brush
270 def EnableFTransf(context
):
271 actObj
= bpy
.context
.active_object
273 if actObj
["BoolTool_FTransform"] == "True":
274 actObj
["BoolTool_FTransform"] = "False"
276 actObj
["BoolTool_FTransform"] = "True"
280 # Apply All Brushes to the Canvas
281 def ApplyAll(context
, list):
284 if isCanvas(selObj
) and selObj
== context
.active_object
:
285 for mod
in selObj
.modifiers
:
286 if "BTool_" in mod
.name
:
287 objDeleteList
.append(mod
.object)
289 bpy
.ops
.object.modifier_apply(modifier
=mod
.name
)
290 except: # if fails the means it is multiuser data
291 context
.active_object
.data
= context
.active_object
.data
.copy() # so just make data unique
292 bpy
.ops
.object.modifier_apply(modifier
=mod
.name
)
293 del selObj
["BoolToolRoot"]
295 for obj
in context
.scene
.objects
:
297 for mod
in obj
.modifiers
:
298 # do not delete brush that is used by another canvas
299 if mod
.type == "BOOLEAN" and mod
.object in objDeleteList
:
300 objDeleteList
.remove(mod
.object) # remove it from deletion
302 bpy
.ops
.object.select_all(action
="DESELECT")
303 for obj
in objDeleteList
:
305 bpy
.ops
.object.delete()
308 # Apply This Brush to the Canvas
309 def ApplyThisBrush(context
, brush
):
310 for obj
in context
.scene
.objects
:
312 for mod
in obj
.modifiers
:
313 if "BTool_" + brush
.name
in mod
.name
:
315 context
.view_layer
.objects
.active
= obj
317 bpy
.ops
.object.modifier_apply(modifier
=mod
.name
)
318 except: # if fails the means it is multiuser data
319 context
.active_object
.data
= context
.active_object
.data
.copy() # so just make data unique
320 bpy
.ops
.object.modifier_apply(modifier
=mod
.name
)
321 bpy
.ops
.object.select_all(action
="TOGGLE")
322 bpy
.ops
.object.select_all(action
="DESELECT")
325 brush
.select_set(True)
326 # bpy.ops.object.delete()
329 # ------------------ Bool Tool OPERATORS --------------------------------------
332 # class BTool_DrawPolyBrush(Operator):
333 # bl_idname = "btool.draw_polybrush"
334 # bl_label = "Draw Poly Brush"
336 # "Draw Polygonal Mask, can be applied to Canvas > Brush or Directly\n"
337 # "Note: ESC to Cancel, Enter to Apply, Right Click to erase the Lines"
341 # store_cont_draw = False
344 # def poll(cls, context):
345 # return context.active_object is not None
347 # def set_cont_draw(self, context, start=False):
348 # # store / restore GP continuous drawing (see T52321)
349 # scene = context.scene
350 # tool_settings = scene.tool_settings
351 # continuous = tool_settings.use_gpencil_continuous_drawing
353 # self.store_cont_draw = continuous
354 # tool_settings.use_gpencil_continuous_drawing = True
356 # tool_settings.use_gpencil_continuous_drawing = self.store_cont_draw
358 # def modal(self, context, event):
360 # actObj = bpy.context.active_object
361 # if self.count == 1:
362 # actObj.select_set(True)
363 # bpy.ops.gpencil.draw("INVOKE_DEFAULT", mode="DRAW_POLY")
365 # if event.type == "RIGHTMOUSE":
366 # # use this to pass to the Grease Pencil eraser (see T52321)
369 # if event.type in {"RET", "NUMPAD_ENTER"}:
371 # bpy.ops.gpencil.convert(type="POLY")
372 # self.set_cont_draw(context)
374 # for obj in context.selected_objects:
375 # if obj.type == "CURVE":
376 # obj.name = "PolyDraw"
377 # bpy.context.view_layer.objects.active = obj
378 # bpy.ops.object.select_all(action="DESELECT")
379 # obj.select_set(True)
380 # bpy.ops.object.convert(target="MESH")
381 # bpy.ops.object.mode_set(mode="EDIT")
382 # bpy.ops.mesh.select_all(action="SELECT")
383 # bpy.ops.mesh.edge_face_add()
384 # bpy.ops.mesh.flip_normals()
385 # bpy.ops.object.mode_set(mode="OBJECT")
386 # bpy.ops.object.origin_set(type="ORIGIN_CENTER_OF_MASS")
387 # bpy.ops.object.modifier_add(type="SOLIDIFY")
388 # for mod in obj.modifiers:
389 # if mod.name == "Solidify":
390 # mod.name = "BTool_PolyBrush"
393 # obj["BoolToolPolyBrush"] = True
395 # bpy.ops.object.select_all(action="DESELECT")
396 # bpy.context.view_layer.objects.active = actObj
397 # bpy.context.view_layer.update()
398 # actObj.select_set(True)
399 # obj.select_set(True)
401 # bpy.context.view_layer.grease_pencil.clear()
402 # bpy.ops.gpencil.data_unlink()
404 # return {"FINISHED"}
406 # if event.type == "ESC":
407 # bpy.ops.ed.undo() # remove o Grease Pencil
408 # self.set_cont_draw(context)
410 # self.report({"INFO"}, "Draw Poly Brush: Operation Cancelled by User")
411 # return {"CANCELLED"}
413 # return {"RUNNING_MODAL"}
415 # def invoke(self, context, event):
417 # self.set_cont_draw(context, start=True)
418 # context.window_manager.modal_handler_add(self)
419 # return {"RUNNING_MODAL"}
421 # self.report({"WARNING"}, "No active object, could not finish")
422 # return {"CANCELLED"}
426 class BTool_FastTransform(Operator
):
427 bl_idname
= "btool.fast_transform"
428 bl_label
= "Fast Transform"
429 bl_description
= "Enable Fast Transform"
431 operator
: StringProperty("")
435 def modal(self
, context
, event
):
437 actObj
= bpy
.context
.active_object
438 useWire
= bpy
.context
.preferences
.addons
[__name__
].preferences
.use_wire
441 if isBrush(actObj
) and actObj
["BoolTool_FTransform"] == "True":
442 EnableThisBrush(bpy
.context
, "False")
444 actObj
.display_type
= "WIRE"
446 actObj
.display_type
= "BOUNDS"
448 if self
.operator
== "Translate":
449 bpy
.ops
.transform
.translate("INVOKE_DEFAULT")
450 if self
.operator
== "Rotate":
451 bpy
.ops
.transform
.rotate("INVOKE_DEFAULT")
452 if self
.operator
== "Scale":
453 bpy
.ops
.transform
.resize("INVOKE_DEFAULT")
455 if event
.type == "LEFTMOUSE":
457 EnableThisBrush(bpy
.context
, "True")
458 actObj
.display_type
= "WIRE"
461 if event
.type in {"RIGHTMOUSE", "ESC"}:
463 EnableThisBrush(bpy
.context
, "True")
464 actObj
.display_type
= "WIRE"
467 return {"RUNNING_MODAL"}
469 def invoke(self
, context
, event
):
471 context
.window_manager
.modal_handler_add(self
)
472 return {"RUNNING_MODAL"}
474 self
.report({"WARNING"}, "No active object, could not finish")
478 # ------------------- Bool Tool OPERATOR CLASSES --------------------------------------------------------
482 # --------------------------------------------------------------------------------------
487 def execute(self
, context
):
488 Operation(context
, self
.mode
)
491 def invoke(self
, context
, event
):
492 if len(context
.selected_objects
) < 2:
493 self
.report({"ERROR"}, "At least two objects must be selected")
496 return self
.execute(context
)
499 class BTool_Union(Operator
, BToolSetup
):
500 bl_idname
= "btool.boolean_union"
501 bl_label
= "Brush Union"
502 bl_description
= "This operator add a union brush to a canvas"
503 bl_options
= {"REGISTER", "UNDO"}
508 class BTool_Inters(Operator
, BToolSetup
):
509 bl_idname
= "btool.boolean_inters"
510 bl_label
= "Brush Intersection"
511 bl_description
= "This operator add a intersect brush to a canvas"
512 bl_options
= {"REGISTER", "UNDO"}
517 class BTool_Diff(Operator
, BToolSetup
):
518 bl_idname
= "btool.boolean_diff"
519 bl_label
= "Brush Difference"
520 bl_description
= "This operator add a difference brush to a canvas"
521 bl_options
= {"REGISTER", "UNDO"}
526 class BTool_Slice(Operator
, BToolSetup
):
527 bl_idname
= "btool.boolean_slice"
528 bl_label
= "Brush Slice"
529 bl_description
= "This operator add a intersect brush to a canvas"
530 bl_options
= {"REGISTER", "UNDO"}
535 # Auto Boolean operators
536 # --------------------------------------------------------------------------------------
541 def objects_prepare(self
):
542 for ob
in bpy
.context
.selected_objects
:
543 if ob
.type != "MESH":
545 bpy
.ops
.object.make_single_user(object=True, obdata
=True)
546 bpy
.ops
.object.convert(target
="MESH")
548 def mesh_selection(self
, ob
, select_action
):
549 obj
= bpy
.context
.active_object
551 bpy
.context
.view_layer
.objects
.active
= ob
552 bpy
.ops
.object.mode_set(mode
="EDIT")
554 bpy
.ops
.mesh
.reveal()
555 bpy
.ops
.mesh
.select_all(action
=select_action
)
557 bpy
.ops
.object.mode_set(mode
="OBJECT")
558 bpy
.context
.view_layer
.objects
.active
= obj
560 def boolean_operation(self
):
561 obj
= bpy
.context
.active_object
562 obj
.select_set(False)
563 obs
= bpy
.context
.selected_objects
565 self
.mesh_selection(obj
, "DESELECT")
568 self
.mesh_selection(ob
, "SELECT")
569 self
.boolean_mod(obj
, ob
, self
.mode
)
573 def boolean_mod(self
, obj
, ob
, mode
, ob_delete
=True):
574 md
= obj
.modifiers
.new("Auto Boolean", "BOOLEAN")
575 md
.show_viewport
= False
579 override
= {"object": obj
}
580 bpy
.ops
.object.modifier_apply(override
, modifier
=md
.name
)
583 bpy
.data
.objects
.remove(ob
)
585 def execute(self
, context
):
586 self
.objects_prepare()
587 self
.boolean_operation()
590 def invoke(self
, context
, event
):
591 if len(context
.selected_objects
) < 2:
592 self
.report({"ERROR"}, "At least two objects must be selected")
595 return self
.execute(context
)
598 class OBJECT_OT_BoolTool_Auto_Union(Operator
, Auto_Boolean
):
599 bl_idname
= "object.booltool_auto_union"
600 bl_label
= "Bool Tool Union"
601 bl_description
= "Combine selected objects"
602 bl_options
= {"REGISTER", "UNDO"}
607 class OBJECT_OT_BoolTool_Auto_Difference(Operator
, Auto_Boolean
):
608 bl_idname
= "object.booltool_auto_difference"
609 bl_label
= "Bool Tool Difference"
610 bl_description
= "Subtract selected objects from active object"
611 bl_options
= {"REGISTER", "UNDO"}
616 class OBJECT_OT_BoolTool_Auto_Intersect(Operator
, Auto_Boolean
):
617 bl_idname
= "object.booltool_auto_intersect"
618 bl_label
= "Bool Tool Intersect"
619 bl_description
= "Keep only intersecting geometry"
620 bl_options
= {"REGISTER", "UNDO"}
625 class OBJECT_OT_BoolTool_Auto_Slice(Operator
, Auto_Boolean
):
626 bl_idname
= "object.booltool_auto_slice"
627 bl_label
= "Bool Tool Slice"
628 bl_description
= "Slice active object along the selected objects"
629 bl_options
= {"REGISTER", "UNDO"}
631 def execute(self
, context
):
632 space_data
= context
.space_data
633 is_local_view
= bool(space_data
.local_view
)
634 self
.objects_prepare()
636 ob1
= context
.active_object
637 ob1
.select_set(False)
638 self
.mesh_selection(ob1
, "DESELECT")
640 for ob2
in context
.selected_objects
:
642 self
.mesh_selection(ob2
, "SELECT")
644 ob1_copy
= ob1
.copy()
645 ob1_copy
.data
= ob1
.data
.copy()
647 for coll
in ob1
.users_collection
:
648 coll
.objects
.link(ob1_copy
)
651 ob1_copy
.local_view_set(space_data
, True)
653 self
.boolean_mod(ob1
, ob2
, "DIFFERENCE", ob_delete
=False)
654 self
.boolean_mod(ob1_copy
, ob2
, "INTERSECT")
655 ob1_copy
.select_set(True)
657 context
.view_layer
.objects
.active
= ob1_copy
662 # Utils Class ---------------------------------------------------------------
664 # Find the Brush Selected in Three View
665 class BTool_FindBrush(Operator
):
666 bl_idname
= "btool.find_brush"
668 bl_description
= "Find the selected brush"
670 obj
: StringProperty("")
673 def poll(cls
, context
):
674 return context
.active_object
is not None
676 def execute(self
, context
):
677 for ob
in bpy
.context
.view_layer
.objects
:
678 if ob
.name
== self
.obj
:
679 bpy
.ops
.object.select_all(action
="TOGGLE")
680 bpy
.ops
.object.select_all(action
="DESELECT")
681 bpy
.context
.view_layer
.objects
.active
= ob
682 ob
.set_select(state
=True)
686 # Move The Modifier in The Stack Up or Down
687 class BTool_MoveStack(Operator
):
688 bl_idname
= "btool.move_stack"
690 bl_description
= "Move this Brush Up/Down in the Stack"
692 modif
: StringProperty("")
693 direction
: StringProperty("")
696 def poll(cls
, context
):
697 return context
.active_object
is not None
699 def execute(self
, context
):
700 if self
.direction
== "UP":
701 bpy
.ops
.object.modifier_move_up(modifier
=self
.modif
)
702 if self
.direction
== "DOWN":
703 bpy
.ops
.object.modifier_move_down(modifier
=self
.modif
)
707 # Enable or Disable a Brush in the Three View
708 class BTool_EnableBrush(Operator
):
709 bl_idname
= "btool.enable_brush"
711 bl_description
= "Removes all BoolTool config assigned to it"
713 thisObj
: StringProperty("")
716 def poll(cls
, context
):
717 return context
.active_object
is not None
719 def execute(self
, context
):
720 # in this case is just one object but the function accept more than one at once
721 EnableBrush(context
, [self
.thisObj
], context
.active_object
)
725 # Enable or Disable a Brush Directly
726 class BTool_EnableThisBrush(Operator
):
727 bl_idname
= "btool.enable_this_brush"
729 bl_description
= "Toggles this brush"
732 def poll(cls
, context
):
733 return context
.active_object
is not None
735 def execute(self
, context
):
736 EnableThisBrush(context
, "None")
740 # Enable or Disable a Brush Directly
741 class BTool_EnableFTransform(Operator
):
742 bl_idname
= "btool.enable_ftransf"
744 bl_description
= "Use Fast Transformations to improve speed"
747 def poll(cls
, context
):
748 return context
.active_object
is not None
750 def execute(self
, context
):
751 EnableFTransf(context
)
755 # Other Operations -------------------------------------------------------
757 # Remove a Brush or a Canvas
758 class BTool_Remove(Operator
):
759 bl_idname
= "btool.remove"
760 bl_label
= "Bool Tool Remove"
761 bl_description
= "Removes all BoolTool config assigned to it"
762 bl_options
= {"UNDO"}
764 thisObj
: StringProperty("")
765 Prop
: StringProperty("")
768 def poll(cls
, context
):
769 return context
.active_object
is not None
771 def execute(self
, context
):
772 Remove(context
, self
.thisObj
, self
.Prop
)
776 # Apply All to Canvas
777 class BTool_AllBrushToMesh(Operator
):
778 bl_idname
= "btool.to_mesh"
779 bl_label
= "Apply All Canvas"
780 bl_description
= "Apply all brushes of this canvas"
781 bl_options
= {"UNDO"}
784 def poll(cls
, context
):
785 return context
.active_object
is not None
787 def execute(self
, context
):
788 lists
= bpy
.context
.selected_objects
789 ApplyAll(context
, lists
)
793 # Apply This Brush to the Canvas
794 class BTool_BrushToMesh(Operator
):
795 bl_idname
= "btool.brush_to_mesh"
796 bl_label
= "Apply this Brush to Canvas"
797 bl_description
= "Apply this brush to the canvas"
798 bl_options
= {"UNDO"}
801 def poll(cls
, context
):
803 if isBrush(context
.active_object
):
808 def execute(self
, context
):
809 ApplyThisBrush(context
, bpy
.context
.active_object
)
814 # Apply This Brush To Mesh
817 # ------------------- MENU CLASSES ------------------------------
820 class VIEW3D_MT_booltool_menu(Menu
):
821 bl_label
= "Bool Tool"
822 bl_idname
= "VIEW3D_MT_booltool_menu"
824 def draw(self
, context
):
827 layout
.label(text
="Auto Boolean")
828 layout
.operator(OBJECT_OT_BoolTool_Auto_Difference
.bl_idname
, text
="Difference", icon
="SELECT_SUBTRACT")
829 layout
.operator(OBJECT_OT_BoolTool_Auto_Union
.bl_idname
, text
="Union", icon
="SELECT_EXTEND")
830 layout
.operator(OBJECT_OT_BoolTool_Auto_Intersect
.bl_idname
, text
="Intersect", icon
="SELECT_INTERSECT")
831 layout
.operator(OBJECT_OT_BoolTool_Auto_Slice
.bl_idname
, text
="Slice", icon
="SELECT_DIFFERENCE")
835 layout
.label(text
="Brush Boolean")
836 layout
.operator(BTool_Diff
.bl_idname
, text
="Difference", icon
="SELECT_SUBTRACT")
837 layout
.operator(BTool_Union
.bl_idname
, text
="Union", icon
="SELECT_EXTEND")
838 layout
.operator(BTool_Inters
.bl_idname
, text
="Intersect", icon
="SELECT_INTERSECT")
839 layout
.operator(BTool_Slice
.bl_idname
, text
="Slice", icon
="SELECT_DIFFERENCE")
841 if isCanvas(context
.active_object
):
843 layout
.operator(BTool_AllBrushToMesh
.bl_idname
, icon
="MOD_LATTICE", text
="Apply All")
844 Rem
= layout
.operator(BTool_Remove
.bl_idname
, icon
="X", text
="Remove All")
848 if isBrush(context
.active_object
):
850 layout
.operator(BTool_BrushToMesh
.bl_idname
, icon
="MOD_LATTICE", text
="Apply Brush")
851 Rem
= layout
.operator(BTool_Remove
.bl_idname
, icon
="X", text
="Remove Brush")
856 def VIEW3D_BoolTool_Menu(self
, context
):
857 self
.layout
.menu(VIEW3D_MT_booltool_menu
.bl_idname
)
860 # ---------------- Toolshelf: Tools ---------------------
863 class VIEW3D_PT_booltool_tools(Panel
):
864 bl_category
= "objectmode"
865 bl_label
= "Bool Tool"
866 bl_space_type
= "VIEW_3D"
867 bl_region_type
= "UI"
868 bl_context
= "objectmode"
869 bl_options
= {'DEFAULT_CLOSED'}
872 def poll(cls
, context
):
873 return context
.active_object
is not None
875 def draw(self
, context
):
878 col
= layout
.column(align
=True)
879 col
.label(text
="Auto Boolean")
880 col
.operator(OBJECT_OT_BoolTool_Auto_Difference
.bl_idname
, text
="Difference", icon
="SELECT_SUBTRACT")
881 col
.operator(OBJECT_OT_BoolTool_Auto_Union
.bl_idname
, text
="Union", icon
="SELECT_EXTEND")
882 col
.operator(OBJECT_OT_BoolTool_Auto_Intersect
.bl_idname
, text
="Intersect", icon
="SELECT_INTERSECT")
883 col
.operator(OBJECT_OT_BoolTool_Auto_Slice
.bl_idname
, text
="Slice", icon
="SELECT_DIFFERENCE")
885 col
= layout
.column(align
=True)
886 col
.label(text
="Brush Boolean")
887 col
.operator(BTool_Diff
.bl_idname
, text
="Difference", icon
="SELECT_SUBTRACT")
888 col
.operator(BTool_Union
.bl_idname
, text
="Union", icon
="SELECT_EXTEND")
889 col
.operator(BTool_Inters
.bl_idname
, text
="Intersect", icon
="SELECT_INTERSECT")
890 col
.operator(BTool_Slice
.bl_idname
, text
="Slice", icon
="SELECT_DIFFERENCE")
892 # TODO Draw Poly Brush
895 # col = main.column(align=True)
896 # col.label(text="Draw:", icon="MESH_CUBE")
898 # col.operator(BTool_DrawPolyBrush.bl_idname, icon="LINE_DATA")
901 # ---------- Toolshelf: Properties --------------------------------------------------------
904 class VIEW3D_PT_booltool_config(Panel
):
905 bl_category
= "objectmode"
906 bl_label
= "Properties"
907 bl_space_type
= "VIEW_3D"
908 bl_region_type
= "UI"
909 bl_context
= "objectmode"
910 bl_parent_id
= "VIEW3D_PT_booltool_tools"
913 def poll(cls
, context
):
914 actObj
= context
.active_object
915 return isCanvas(actObj
) or isBrush(actObj
) # or isPolyBrush(actObj)
917 def draw(self
, context
):
919 actObj
= context
.active_object
921 row
= layout
.row(align
=True)
925 row
.label(text
="CANVAS", icon
="MESH_GRID")
927 row
.prop(context
.scene
, "BoolHide", text
="Hide Bool objects")
928 row
= layout
.row(align
=True)
929 row
.operator(BTool_AllBrushToMesh
.bl_idname
, icon
="MOD_LATTICE", text
="Apply All")
931 row
= layout
.row(align
=True)
932 Rem
= row
.operator(BTool_Remove
.bl_idname
, icon
="X", text
="Remove All")
941 if actObj
["BoolToolBrush"] == "DIFFERENCE":
942 icon
= "SELECT_SUBTRACT"
943 elif actObj
["BoolToolBrush"] == "UNION":
944 icon
= "SELECT_EXTEND"
945 elif actObj
["BoolToolBrush"] == "INTERSECT":
946 icon
= "SELECT_INTERSECT"
947 elif actObj
["BoolToolBrush"] == "SLICE":
948 icon
= "SELECT_DIFFERENCE"
950 row
.label(text
="BRUSH", icon
=icon
)
952 if actObj
["BoolTool_FTransform"] == "True":
960 row
= layout
.row(align
=True)
961 row
.operator(BTool_EnableFTransform
.bl_idname
, text
="Fast Vis", icon
=icon
)
962 row
.operator(BTool_EnableThisBrush
.bl_idname
, text
="Enable", icon
="HIDE_OFF")
964 row
.operator(BTool_EnableThisBrush
.bl_idname
, icon
="HIDE_OFF")
966 layout
.operator(BTool_BrushToMesh
.bl_idname
, icon
="MOD_LATTICE", text
="Apply Brush")
967 Rem
= layout
.operator(BTool_Remove
.bl_idname
, icon
="X", text
="Remove Brush")
972 # if isPolyBrush(actObj):
973 # layout.label(text="POLY BRUSH", icon="LINE_DATA")
974 # mod = actObj.modifiers["BTool_PolyBrush"]
975 # layout.prop(mod, "thickness", text="Size")
978 # ---------- Toolshelf: Brush Viewer -------------------------------------------------------
981 class VIEW3D_PT_booltool_bviewer(Panel
):
982 bl_category
= "objectmode"
983 bl_label
= "Brush Viewer"
984 bl_space_type
= "VIEW_3D"
985 bl_region_type
= "UI"
986 bl_context
= "objectmode"
987 bl_parent_id
= "VIEW3D_PT_booltool_tools"
990 def poll(cls
, context
):
991 actObj
= bpy
.context
.active_object
998 def draw(self
, context
):
1000 actObj
= bpy
.context
.active_object
1002 if isCanvas(actObj
):
1004 for mod
in actObj
.modifiers
:
1005 container
= self
.layout
.box()
1006 row
= container
.row(align
=True)
1008 if "BTool_" in mod
.name
:
1010 if mod
.operation
== "DIFFERENCE":
1011 icon
= "SELECT_SUBTRACT"
1012 elif mod
.operation
== "UNION":
1013 icon
= "SELECT_EXTEND"
1014 elif mod
.operation
== "INTERSECT":
1015 icon
= "SELECT_INTERSECT"
1016 elif mod
.operation
== "SLICE":
1017 icon
= "SELECT_DIFFERENCE"
1019 objSelect
= row
.operator("btool.find_brush", text
=mod
.object.name
, icon
=icon
, emboss
=False)
1020 objSelect
.obj
= mod
.object.name
1022 EnableIcon
= "RESTRICT_VIEW_ON"
1023 if mod
.show_viewport
:
1024 EnableIcon
= "RESTRICT_VIEW_OFF"
1025 Enable
= row
.operator(BTool_EnableBrush
.bl_idname
, icon
=EnableIcon
, emboss
=False)
1026 Enable
.thisObj
= mod
.object.name
1028 Remove
= row
.operator("btool.remove", text
="", icon
="X", emboss
=False)
1029 Remove
.thisObj
= mod
.object.name
1030 Remove
.Prop
= "THIS"
1033 row
.label(text
=mod
.name
)
1035 Up
= row
.operator("btool.move_stack", icon
="TRIA_UP", emboss
=False)
1039 Dw
= row
.operator("btool.move_stack", icon
="TRIA_DOWN", emboss
=False)
1041 Dw
.direction
= "DOWN"
1044 # ------------------ BOOL TOOL ADD-ON PREFERENCES ----------------------------
1049 ("Menu", "Ctrl Shift B"),
1051 ("Auto Operators", None),
1052 ("Difference", "Ctrl Shift Num -"),
1053 ("Union", "Ctrl Shift Num +"),
1054 ("Intersect", "Ctrl Shift Num *"),
1055 ("Slice", "Ctrl Shift Num /"),
1057 ("Brush Operators", None),
1058 ("Difference", "Ctrl Num -"),
1059 ("Union", "Ctrl Num +"),
1060 ("Intersect", "Ctrl Num *"),
1061 ("Slice", "Ctrl Num /"),
1062 ("Brush To Mesh", "Ctrl Num Enter"),
1063 ("All Brushes To Mesh", "Ctrl Shift Num Enter"),
1067 def UpdateBoolTool_Pref(self
, context
):
1068 if self
.fast_transform
:
1074 # Define Panel classes for updating
1076 VIEW3D_PT_booltool_tools
,
1077 VIEW3D_PT_booltool_config
,
1078 VIEW3D_PT_booltool_bviewer
,
1082 def update_panels(self
, context
):
1084 for panel
in panels
:
1085 if "bl_rna" in panel
.__dict
__:
1086 bpy
.utils
.unregister_class(panel
)
1088 for panel
in panels
:
1089 panel
.bl_category
= context
.preferences
.addons
[
1091 ].preferences
.category
1092 bpy
.utils
.register_class(panel
)
1094 except Exception as e
:
1095 message
= "Bool Tool: Updating Panel locations has failed"
1096 print("\n[{}]\n{}\n\nError:\n{}".format(__name__
, message
, e
))
1099 def icon_tria(prop
):
1105 class PREFS_BoolTool_Props(AddonPreferences
):
1106 bl_idname
= __name__
1108 fast_transform
: BoolProperty(
1109 name
="Fast Transformations",
1110 update
=UpdateBoolTool_Pref
,
1111 description
="Replace the Transform HotKeys (G,R,S)\n"
1112 "for a custom version that can optimize the visualization of Brushes",
1114 use_wire
: BoolProperty(
1115 name
="Display As Wirewrame",
1116 description
="Display brush as wireframe instead of bounding box",
1118 category
: StringProperty(
1120 description
="Set sidebar tab name",
1122 update
=update_panels
,
1124 show_shortcuts
: BoolProperty(name
="Shortcuts")
1126 def draw(self
, context
):
1127 layout
= self
.layout
1128 layout
.use_property_split
= True
1129 layout
.use_property_decorate
= False
1131 col
= layout
.column()
1132 col
.prop(self
, "category")
1133 col
.prop(self
, "fast_transform")
1134 col
.prop(self
, "use_wire")
1136 col
= layout
.column()
1138 col
.use_property_split
= False
1139 col
.prop(self
, "show_shortcuts", icon
=icon_tria(self
.show_shortcuts
))
1141 if self
.show_shortcuts
:
1143 col
= layout
.column()
1145 for key_name
, key_comb
in shortcut_list
:
1146 if key_comb
is None:
1148 col
.label(text
=key_name
)
1150 row
= col
.row(align
=True)
1152 row
.box().label(text
=key_name
)
1153 row
.box().label(text
=key_comb
)
1156 # ------------------- Class List ------------------------------------------------
1159 PREFS_BoolTool_Props
,
1160 VIEW3D_MT_booltool_menu
,
1161 VIEW3D_PT_booltool_tools
,
1162 VIEW3D_PT_booltool_config
,
1163 VIEW3D_PT_booltool_bviewer
,
1164 OBJECT_OT_BoolTool_Auto_Union
,
1165 OBJECT_OT_BoolTool_Auto_Difference
,
1166 OBJECT_OT_BoolTool_Auto_Intersect
,
1167 OBJECT_OT_BoolTool_Auto_Slice
,
1172 # TODO Draw Poly Brush
1173 # BTool_DrawPolyBrush,
1175 BTool_AllBrushToMesh
,
1180 BTool_EnableThisBrush
,
1181 BTool_EnableFTransform
,
1182 BTool_FastTransform
,
1186 # ------------------- REGISTER ------------------------------------------------
1189 addon_keymapsFastT
= []
1192 # Fast Transform HotKeys Register
1193 def RegisterFastT():
1194 wm
= bpy
.context
.window_manager
1195 km
= wm
.keyconfigs
.addon
.keymaps
.new(name
="Object Mode", space_type
="EMPTY")
1197 kmi
= km
.keymap_items
.new(BTool_FastTransform
.bl_idname
, "G", "PRESS")
1198 kmi
.properties
.operator
= "Translate"
1199 addon_keymapsFastT
.append((km
, kmi
))
1201 kmi
= km
.keymap_items
.new(BTool_FastTransform
.bl_idname
, "R", "PRESS")
1202 kmi
.properties
.operator
= "Rotate"
1203 addon_keymapsFastT
.append((km
, kmi
))
1205 kmi
= km
.keymap_items
.new(BTool_FastTransform
.bl_idname
, "S", "PRESS")
1206 kmi
.properties
.operator
= "Scale"
1207 addon_keymapsFastT
.append((km
, kmi
))
1210 # Fast Transform HotKeys UnRegister
1211 def UnRegisterFastT():
1212 wm
= bpy
.context
.window_manager
1213 kc
= wm
.keyconfigs
.addon
1215 for km
, kmi
in addon_keymapsFastT
:
1216 km
.keymap_items
.remove(kmi
)
1218 addon_keymapsFastT
.clear()
1223 bpy
.utils
.register_class(cls
)
1224 update_panels(None, bpy
.context
)
1227 bpy
.types
.Scene
.BoolHide
= BoolProperty(
1229 description
="Hide boolean objects",
1230 update
=update_BoolHide
,
1232 bpy
.types
.VIEW3D_MT_object
.append(VIEW3D_BoolTool_Menu
)
1234 wm
= bpy
.context
.window_manager
1235 kc
= wm
.keyconfigs
.addon
1237 # create the boolean menu hotkey
1239 km
= kc
.keymaps
.new(name
="Object Mode")
1241 kmi
= km
.keymap_items
.new("wm.call_menu", "B", "PRESS", ctrl
=True, shift
=True)
1242 kmi
.properties
.name
= "VIEW3D_MT_booltool_menu"
1243 addon_keymaps
.append((km
, kmi
))
1246 kmi
= km
.keymap_items
.new(BTool_Union
.bl_idname
, "NUMPAD_PLUS", "PRESS", ctrl
=True)
1247 addon_keymaps
.append((km
, kmi
))
1248 kmi
= km
.keymap_items
.new(BTool_Diff
.bl_idname
, "NUMPAD_MINUS", "PRESS", ctrl
=True)
1249 addon_keymaps
.append((km
, kmi
))
1250 kmi
= km
.keymap_items
.new(BTool_Inters
.bl_idname
, "NUMPAD_ASTERIX", "PRESS", ctrl
=True)
1251 addon_keymaps
.append((km
, kmi
))
1252 kmi
= km
.keymap_items
.new(BTool_Slice
.bl_idname
, "NUMPAD_SLASH", "PRESS", ctrl
=True)
1253 addon_keymaps
.append((km
, kmi
))
1254 kmi
= km
.keymap_items
.new(BTool_BrushToMesh
.bl_idname
, "NUMPAD_ENTER", "PRESS", ctrl
=True)
1255 addon_keymaps
.append((km
, kmi
))
1256 kmi
= km
.keymap_items
.new(
1257 BTool_AllBrushToMesh
.bl_idname
,
1263 addon_keymaps
.append((km
, kmi
))
1266 kmi
= km
.keymap_items
.new(
1267 OBJECT_OT_BoolTool_Auto_Union
.bl_idname
,
1273 addon_keymaps
.append((km
, kmi
))
1274 kmi
= km
.keymap_items
.new(
1275 OBJECT_OT_BoolTool_Auto_Difference
.bl_idname
,
1281 addon_keymaps
.append((km
, kmi
))
1282 kmi
= km
.keymap_items
.new(
1283 OBJECT_OT_BoolTool_Auto_Intersect
.bl_idname
,
1289 addon_keymaps
.append((km
, kmi
))
1290 kmi
= km
.keymap_items
.new(
1291 OBJECT_OT_BoolTool_Auto_Slice
.bl_idname
,
1297 addon_keymaps
.append((km
, kmi
))
1302 # remove keymaps when add-on is deactivated
1303 wm
= bpy
.context
.window_manager
1304 kc
= wm
.keyconfigs
.addon
1306 for km
, kmi
in addon_keymaps
:
1307 km
.keymap_items
.remove(kmi
)
1309 addon_keymaps
.clear()
1311 bpy
.types
.VIEW3D_MT_object
.remove(VIEW3D_BoolTool_Menu
)
1312 del bpy
.types
.Scene
.BoolHide
1315 bpy
.utils
.unregister_class(cls
)
1318 if __name__
== "__main__":