Cleanup: tabs -> spaces
[blender-addons.git] / object_boolean_tools.py
blob7ea231e94af15c3dd7a560235695e1135ed14a06
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 #####
19 # <pep8 compliant>
21 bl_info = {
22 "name": "Bool Tool",
23 "author": "Vitor Balbio, Mikhail Rachinskiy, TynkaTopi, Meta-Androcto, Simon Appelt",
24 "version": (0, 4, 1),
25 "blender": (2, 80, 0),
26 "location": "View3D > Sidebar > Edit Tab",
27 "description": "Bool Tool Hotkey: Ctrl Shift B",
28 "doc_url": "https://docs.blender.org/manual/en/dev/addons/"
29 "object/bool_tools.html",
30 "category": "Object",
33 import bpy
34 from bpy.types import (
35 AddonPreferences,
36 Operator,
37 Panel,
38 Menu,
40 from bpy.props import (
41 BoolProperty,
42 StringProperty,
46 # ------------------- Bool Tool FUNCTIONS -------------------------
47 # Utils:
49 # Hide boolean objects
50 def update_BoolHide(self, context):
51 ao = context.view_layer.objects.active
52 objs = [i.object for i in ao.modifiers if i.type == "BOOLEAN"]
53 hide_state = context.scene.BoolHide
55 for o in objs:
56 o.hide_viewport = hide_state
59 def isCanvas(_obj):
60 try:
61 if _obj["BoolToolRoot"]:
62 return True
63 except:
64 return False
67 def isBrush(_obj):
68 try:
69 if _obj["BoolToolBrush"]:
70 return True
71 except:
72 return False
75 # TODO
76 # def isPolyBrush(_obj):
77 # try:
78 # if _obj["BoolToolPolyBrush"]:
79 # return True
80 # except:
81 # return False
84 def cycles_visibility_set(ob, value=False):
85 if not hasattr(ob, "cycles_visibility"):
86 return
88 vis = ob.cycles_visibility
90 vis.camera = value
91 vis.diffuse = value
92 vis.glossy = value
93 vis.shadow = value
94 vis.transmission = value
95 vis.scatter = value
98 def BT_ObjectByName(obj):
99 for ob in bpy.context.view_layer.objects:
100 if isCanvas(ob) or isBrush(ob):
101 if ob.name == obj:
102 return ob
105 def FindCanvas(obj):
106 for ob in bpy.context.view_layer.objects:
107 if isCanvas(ob):
108 for mod in ob.modifiers:
109 if "BTool_" in mod.name:
110 if obj.name in mod.name:
111 return ob
114 def isFTransf():
115 preferences = bpy.context.preferences
116 addons = preferences.addons
117 addon_prefs = addons[__name__].preferences
118 if addon_prefs.fast_transform:
119 return True
120 else:
121 return False
124 def ConvertToMesh(obj):
125 act = bpy.context.view_layer.objects.active
126 bpy.context.view_layer.objects.active = obj
127 bpy.ops.object.convert(target="MESH")
128 bpy.context.view_layer.objects.active = act
131 # Do the Union, Difference and Intersection Operations with a Brush
132 def Operation(context, _operation):
133 prefs = context.preferences.addons[__name__].preferences
134 useWire = prefs.use_wire
136 for selObj in context.selected_objects:
137 if (
138 selObj != context.active_object and
139 (selObj.type == "MESH" or selObj.type == "CURVE")
141 if selObj.type == "CURVE":
142 ConvertToMesh(selObj)
143 actObj = context.active_object
144 selObj.hide_render = True
146 if useWire:
147 selObj.display_type = "WIRE"
148 else:
149 selObj.display_type = "BOUNDS"
151 cycles_visibility_set(selObj, value=False)
153 if _operation == "SLICE":
154 # copies instance_collection property(empty), but group property is empty (users_group = None)
155 clone = actObj.copy()
156 context.collection.objects.link(clone)
158 space_data = context.space_data
159 is_local_view = bool(space_data.local_view)
161 if is_local_view:
162 clone.local_view_set(space_data, True)
164 sliceMod = clone.modifiers.new("BTool_" + selObj.name, "BOOLEAN") # add mod to clone obj
165 sliceMod.object = selObj
166 sliceMod.operation = "DIFFERENCE"
167 clone["BoolToolRoot"] = True
169 newMod = actObj.modifiers.new("BTool_" + selObj.name, "BOOLEAN")
170 newMod.object = selObj
172 if _operation == "SLICE":
173 newMod.operation = "INTERSECT"
174 else:
175 newMod.operation = _operation
177 actObj["BoolToolRoot"] = True
178 selObj["BoolToolBrush"] = _operation
179 selObj["BoolTool_FTransform"] = "False"
182 # Remove Objects form the BoolTool System
183 def Remove(context, thisObj_name, Prop):
184 # Find the Brush pointed in the Tree View and Restore it, active is the Canvas
185 actObj = context.active_object
187 # Restore the Brush
188 def RemoveThis(_thisObj_name):
189 for obj in bpy.context.view_layer.objects:
190 # if it's the brush object
191 if obj.name == _thisObj_name:
192 obj.display_type = "TEXTURED"
193 del obj["BoolToolBrush"]
194 del obj["BoolTool_FTransform"]
195 cycles_visibility_set(obj, value=True)
197 # Remove it from the Canvas
198 for mod in actObj.modifiers:
199 if "BTool_" in mod.name:
200 if _thisObj_name in mod.name:
201 actObj.modifiers.remove(mod)
203 if Prop == "THIS":
204 RemoveThis(thisObj_name)
206 # If the remove was called from the Properties:
207 else:
208 # Remove the Brush Property
209 if Prop == "BRUSH":
210 Canvas = FindCanvas(actObj)
212 if Canvas:
213 for mod in Canvas.modifiers:
214 if "BTool_" in mod.name and actObj.name in mod.name:
215 Canvas.modifiers.remove(mod)
217 actObj.display_type = "TEXTURED"
218 del actObj["BoolToolBrush"]
219 del actObj["BoolTool_FTransform"]
220 cycles_visibility_set(actObj, value=True)
222 if Prop == "CANVAS":
223 for mod in actObj.modifiers:
224 if "BTool_" in mod.name:
225 RemoveThis(mod.object.name)
228 # Toggle the Enable the Brush Object Property
229 def EnableBrush(context, objList, canvas):
230 for obj in objList:
231 for mod in canvas.modifiers:
232 if "BTool_" in mod.name and mod.object.name == obj:
234 if mod.show_viewport:
235 mod.show_viewport = False
236 mod.show_render = False
237 else:
238 mod.show_viewport = True
239 mod.show_render = True
242 # Find the Canvas and Enable this Brush
243 def EnableThisBrush(context, set):
244 canvas = None
245 for obj in bpy.context.view_layer.objects:
246 if obj != bpy.context.active_object:
247 if isCanvas(obj):
248 for mod in obj.modifiers:
249 if "BTool_" in mod.name:
250 if mod.object == bpy.context.active_object:
251 canvas = obj
253 for mod in canvas.modifiers:
254 if "BTool_" in mod.name:
255 if mod.object == bpy.context.active_object:
256 if set == "None":
257 if mod.show_viewport:
258 mod.show_viewport = False
259 mod.show_render = False
260 else:
261 mod.show_viewport = True
262 mod.show_render = True
263 else:
264 if set == "True":
265 mod.show_viewport = True
266 else:
267 mod.show_viewport = False
268 return
271 # Toggle the Fast Transform Property of the Active Brush
272 def EnableFTransf(context):
273 actObj = bpy.context.active_object
275 if actObj["BoolTool_FTransform"] == "True":
276 actObj["BoolTool_FTransform"] = "False"
277 else:
278 actObj["BoolTool_FTransform"] = "True"
279 return
282 # Apply All Brushes to the Canvas
283 def ApplyAll(context, list):
284 objDeleteList = []
285 for selObj in list:
286 if isCanvas(selObj) and selObj == context.active_object:
287 for mod in selObj.modifiers:
288 if "BTool_" in mod.name:
289 objDeleteList.append(mod.object)
290 try:
291 bpy.ops.object.modifier_apply(modifier=mod.name)
292 except: # if fails the means it is multiuser data
293 context.active_object.data = context.active_object.data.copy() # so just make data unique
294 bpy.ops.object.modifier_apply(modifier=mod.name)
295 del selObj["BoolToolRoot"]
297 for obj in context.scene.objects:
298 if isCanvas(obj):
299 for mod in obj.modifiers:
300 # do not delete brush that is used by another canvas
301 if mod.type == "BOOLEAN" and mod.object in objDeleteList:
302 objDeleteList.remove(mod.object) # remove it from deletion
304 bpy.ops.object.select_all(action="DESELECT")
305 for obj in objDeleteList:
306 obj.select_set(True)
307 bpy.ops.object.delete()
310 # Apply This Brush to the Canvas
311 def ApplyThisBrush(context, brush):
312 for obj in context.scene.objects:
313 if isCanvas(obj):
314 for mod in obj.modifiers:
315 if "BTool_" + brush.name in mod.name:
316 # Apply This Brush
317 context.view_layer.objects.active = obj
318 try:
319 bpy.ops.object.modifier_apply(modifier=mod.name)
320 except: # if fails the means it is multiuser data
321 context.active_object.data = context.active_object.data.copy() # so just make data unique
322 bpy.ops.object.modifier_apply(modifier=mod.name)
323 bpy.ops.object.select_all(action="TOGGLE")
324 bpy.ops.object.select_all(action="DESELECT")
326 # Garbage Collector
327 brush.select_set(True)
328 # bpy.ops.object.delete()
331 # ------------------ Bool Tool OPERATORS --------------------------------------
333 # TODO
334 # class BTool_DrawPolyBrush(Operator):
335 # bl_idname = "btool.draw_polybrush"
336 # bl_label = "Draw Poly Brush"
337 # bl_description = (
338 # "Draw Polygonal Mask, can be applied to Canvas > Brush or Directly\n"
339 # "Note: ESC to Cancel, Enter to Apply, Right Click to erase the Lines"
342 # count = 0
343 # store_cont_draw = False
345 # @classmethod
346 # def poll(cls, context):
347 # return context.active_object is not None
349 # def set_cont_draw(self, context, start=False):
350 # # store / restore GP continuous drawing (see T52321)
351 # scene = context.scene
352 # tool_settings = scene.tool_settings
353 # continuous = tool_settings.use_gpencil_continuous_drawing
354 # if start:
355 # self.store_cont_draw = continuous
356 # tool_settings.use_gpencil_continuous_drawing = True
357 # else:
358 # tool_settings.use_gpencil_continuous_drawing = self.store_cont_draw
360 # def modal(self, context, event):
361 # self.count += 1
362 # actObj = bpy.context.active_object
363 # if self.count == 1:
364 # actObj.select_set(True)
365 # bpy.ops.gpencil.draw("INVOKE_DEFAULT", mode="DRAW_POLY")
367 # if event.type == "RIGHTMOUSE":
368 # # use this to pass to the Grease Pencil eraser (see T52321)
369 # pass
371 # if event.type in {"RET", "NUMPAD_ENTER"}:
373 # bpy.ops.gpencil.convert(type="POLY")
374 # self.set_cont_draw(context)
376 # for obj in context.selected_objects:
377 # if obj.type == "CURVE":
378 # obj.name = "PolyDraw"
379 # bpy.context.view_layer.objects.active = obj
380 # bpy.ops.object.select_all(action="DESELECT")
381 # obj.select_set(True)
382 # bpy.ops.object.convert(target="MESH")
383 # bpy.ops.object.mode_set(mode="EDIT")
384 # bpy.ops.mesh.select_all(action="SELECT")
385 # bpy.ops.mesh.edge_face_add()
386 # bpy.ops.mesh.flip_normals()
387 # bpy.ops.object.mode_set(mode="OBJECT")
388 # bpy.ops.object.origin_set(type="ORIGIN_CENTER_OF_MASS")
389 # bpy.ops.object.modifier_add(type="SOLIDIFY")
390 # for mod in obj.modifiers:
391 # if mod.name == "Solidify":
392 # mod.name = "BTool_PolyBrush"
393 # mod.thickness = 1
394 # mod.offset = 0
395 # obj["BoolToolPolyBrush"] = True
397 # bpy.ops.object.select_all(action="DESELECT")
398 # bpy.context.view_layer.objects.active = actObj
399 # bpy.context.view_layer.update()
400 # actObj.select_set(True)
401 # obj.select_set(True)
403 # bpy.context.view_layer.grease_pencil.clear()
404 # bpy.ops.gpencil.data_unlink()
406 # return {"FINISHED"}
408 # if event.type == "ESC":
409 # bpy.ops.ed.undo() # remove o Grease Pencil
410 # self.set_cont_draw(context)
412 # self.report({"INFO"}, "Draw Poly Brush: Operation Cancelled by User")
413 # return {"CANCELLED"}
415 # return {"RUNNING_MODAL"}
417 # def invoke(self, context, event):
418 # if context.object:
419 # self.set_cont_draw(context, start=True)
420 # context.window_manager.modal_handler_add(self)
421 # return {"RUNNING_MODAL"}
422 # else:
423 # self.report({"WARNING"}, "No active object, could not finish")
424 # return {"CANCELLED"}
427 # Fast Transform
428 class BTool_FastTransform(Operator):
429 bl_idname = "btool.fast_transform"
430 bl_label = "Fast Transform"
431 bl_description = "Enable Fast Transform"
433 operator: StringProperty("")
435 count = 0
437 def modal(self, context, event):
438 self.count += 1
439 actObj = bpy.context.active_object
440 useWire = bpy.context.preferences.addons[__name__].preferences.use_wire
441 if self.count == 1:
443 if isBrush(actObj) and actObj["BoolTool_FTransform"] == "True":
444 EnableThisBrush(bpy.context, "False")
445 if useWire:
446 actObj.display_type = "WIRE"
447 else:
448 actObj.display_type = "BOUNDS"
450 if self.operator == "Translate":
451 bpy.ops.transform.translate("INVOKE_DEFAULT")
452 if self.operator == "Rotate":
453 bpy.ops.transform.rotate("INVOKE_DEFAULT")
454 if self.operator == "Scale":
455 bpy.ops.transform.resize("INVOKE_DEFAULT")
457 if event.type == "LEFTMOUSE":
458 if isBrush(actObj):
459 EnableThisBrush(bpy.context, "True")
460 actObj.display_type = "WIRE"
461 return {"FINISHED"}
463 if event.type in {"RIGHTMOUSE", "ESC"}:
464 if isBrush(actObj):
465 EnableThisBrush(bpy.context, "True")
466 actObj.display_type = "WIRE"
467 return {"CANCELLED"}
469 return {"RUNNING_MODAL"}
471 def invoke(self, context, event):
472 if context.object:
473 context.window_manager.modal_handler_add(self)
474 return {"RUNNING_MODAL"}
475 else:
476 self.report({"WARNING"}, "No active object, could not finish")
477 return {"CANCELLED"}
480 # ------------------- Bool Tool OPERATOR CLASSES --------------------------------------------------------
483 # Brush operators
484 # --------------------------------------------------------------------------------------
487 class BToolSetup():
489 def execute(self, context):
490 Operation(context, self.mode)
491 return {"FINISHED"}
493 def invoke(self, context, event):
494 if len(context.selected_objects) < 2:
495 self.report({"ERROR"}, "At least two objects must be selected")
496 return {"CANCELLED"}
498 return self.execute(context)
501 class BTool_Union(Operator, BToolSetup):
502 bl_idname = "btool.boolean_union"
503 bl_label = "Brush Union"
504 bl_description = "This operator add a union brush to a canvas"
505 bl_options = {"REGISTER", "UNDO"}
507 mode = "UNION"
510 class BTool_Inters(Operator, BToolSetup):
511 bl_idname = "btool.boolean_inters"
512 bl_label = "Brush Intersection"
513 bl_description = "This operator add a intersect brush to a canvas"
514 bl_options = {"REGISTER", "UNDO"}
516 mode = "INTERSECT"
519 class BTool_Diff(Operator, BToolSetup):
520 bl_idname = "btool.boolean_diff"
521 bl_label = "Brush Difference"
522 bl_description = "This operator add a difference brush to a canvas"
523 bl_options = {"REGISTER", "UNDO"}
525 mode = "DIFFERENCE"
528 class BTool_Slice(Operator, BToolSetup):
529 bl_idname = "btool.boolean_slice"
530 bl_label = "Brush Slice"
531 bl_description = "This operator add a intersect brush to a canvas"
532 bl_options = {"REGISTER", "UNDO"}
534 mode = "SLICE"
537 # Auto Boolean operators
538 # --------------------------------------------------------------------------------------
541 class Auto_Boolean:
543 def objects_prepare(self):
544 for ob in bpy.context.selected_objects:
545 if ob.type != "MESH":
546 ob.select_set(False)
547 bpy.ops.object.make_single_user(object=True, obdata=True)
548 bpy.ops.object.convert(target="MESH")
550 def mesh_selection(self, ob, select_action):
551 obj = bpy.context.active_object
553 bpy.context.view_layer.objects.active = ob
554 bpy.ops.object.mode_set(mode="EDIT")
556 bpy.ops.mesh.reveal()
557 bpy.ops.mesh.select_all(action=select_action)
559 bpy.ops.object.mode_set(mode="OBJECT")
560 bpy.context.view_layer.objects.active = obj
562 def boolean_operation(self):
563 obj = bpy.context.active_object
564 obj.select_set(False)
565 obs = bpy.context.selected_objects
567 self.mesh_selection(obj, "DESELECT")
569 for ob in obs:
570 self.mesh_selection(ob, "SELECT")
571 self.boolean_mod(obj, ob, self.mode)
573 obj.select_set(True)
575 def boolean_mod(self, obj, ob, mode, ob_delete=True):
576 md = obj.modifiers.new("Auto Boolean", "BOOLEAN")
577 md.show_viewport = False
578 md.operation = mode
579 md.object = ob
581 override = {"object": obj}
582 bpy.ops.object.modifier_apply(override, modifier=md.name)
584 if ob_delete:
585 bpy.data.objects.remove(ob)
587 def execute(self, context):
588 self.objects_prepare()
589 self.boolean_operation()
590 return {"FINISHED"}
592 def invoke(self, context, event):
593 if len(context.selected_objects) < 2:
594 self.report({"ERROR"}, "At least two objects must be selected")
595 return {"CANCELLED"}
597 return self.execute(context)
600 class OBJECT_OT_BoolTool_Auto_Union(Operator, Auto_Boolean):
601 bl_idname = "object.booltool_auto_union"
602 bl_label = "Bool Tool Union"
603 bl_description = "Combine selected objects"
604 bl_options = {"REGISTER", "UNDO"}
606 mode = "UNION"
609 class OBJECT_OT_BoolTool_Auto_Difference(Operator, Auto_Boolean):
610 bl_idname = "object.booltool_auto_difference"
611 bl_label = "Bool Tool Difference"
612 bl_description = "Subtract selected objects from active object"
613 bl_options = {"REGISTER", "UNDO"}
615 mode = "DIFFERENCE"
618 class OBJECT_OT_BoolTool_Auto_Intersect(Operator, Auto_Boolean):
619 bl_idname = "object.booltool_auto_intersect"
620 bl_label = "Bool Tool Intersect"
621 bl_description = "Keep only intersecting geometry"
622 bl_options = {"REGISTER", "UNDO"}
624 mode = "INTERSECT"
627 class OBJECT_OT_BoolTool_Auto_Slice(Operator, Auto_Boolean):
628 bl_idname = "object.booltool_auto_slice"
629 bl_label = "Bool Tool Slice"
630 bl_description = "Slice active object along the selected objects"
631 bl_options = {"REGISTER", "UNDO"}
633 def execute(self, context):
634 space_data = context.space_data
635 is_local_view = bool(space_data.local_view)
636 self.objects_prepare()
638 ob1 = context.active_object
639 ob1.select_set(False)
640 self.mesh_selection(ob1, "DESELECT")
642 for ob2 in context.selected_objects:
644 self.mesh_selection(ob2, "SELECT")
646 ob1_copy = ob1.copy()
647 ob1_copy.data = ob1.data.copy()
649 for coll in ob1.users_collection:
650 coll.objects.link(ob1_copy)
652 if is_local_view:
653 ob1_copy.local_view_set(space_data, True)
655 self.boolean_mod(ob1, ob2, "DIFFERENCE", ob_delete=False)
656 self.boolean_mod(ob1_copy, ob2, "INTERSECT")
657 ob1_copy.select_set(True)
659 context.view_layer.objects.active = ob1_copy
661 return {"FINISHED"}
664 # Utils Class ---------------------------------------------------------------
666 # Find the Brush Selected in Three View
667 class BTool_FindBrush(Operator):
668 bl_idname = "btool.find_brush"
669 bl_label = ""
670 bl_description = "Find the selected brush"
672 obj: StringProperty("")
674 @classmethod
675 def poll(cls, context):
676 return context.active_object is not None
678 def execute(self, context):
679 for ob in bpy.context.view_layer.objects:
680 if ob.name == self.obj:
681 bpy.ops.object.select_all(action="TOGGLE")
682 bpy.ops.object.select_all(action="DESELECT")
683 bpy.context.view_layer.objects.active = ob
684 ob.set_select(state=True)
685 return {"FINISHED"}
688 # Move The Modifier in The Stack Up or Down
689 class BTool_MoveStack(Operator):
690 bl_idname = "btool.move_stack"
691 bl_label = ""
692 bl_description = "Move this Brush Up/Down in the Stack"
694 modif: StringProperty("")
695 direction: StringProperty("")
697 @classmethod
698 def poll(cls, context):
699 return context.active_object is not None
701 def execute(self, context):
702 if self.direction == "UP":
703 bpy.ops.object.modifier_move_up(modifier=self.modif)
704 if self.direction == "DOWN":
705 bpy.ops.object.modifier_move_down(modifier=self.modif)
706 return {"FINISHED"}
709 # Enable or Disable a Brush in the Three View
710 class BTool_EnableBrush(Operator):
711 bl_idname = "btool.enable_brush"
712 bl_label = ""
713 bl_description = "Removes all BoolTool config assigned to it"
715 thisObj: StringProperty("")
717 @classmethod
718 def poll(cls, context):
719 return context.active_object is not None
721 def execute(self, context):
722 # in this case is just one object but the function accept more than one at once
723 EnableBrush(context, [self.thisObj], context.active_object)
724 return {"FINISHED"}
727 # Enable or Disable a Brush Directly
728 class BTool_EnableThisBrush(Operator):
729 bl_idname = "btool.enable_this_brush"
730 bl_label = ""
731 bl_description = "Toggles this brush"
733 @classmethod
734 def poll(cls, context):
735 return context.active_object is not None
737 def execute(self, context):
738 EnableThisBrush(context, "None")
739 return {"FINISHED"}
742 # Enable or Disable a Brush Directly
743 class BTool_EnableFTransform(Operator):
744 bl_idname = "btool.enable_ftransf"
745 bl_label = ""
746 bl_description = "Use Fast Transformations to improve speed"
748 @classmethod
749 def poll(cls, context):
750 return context.active_object is not None
752 def execute(self, context):
753 EnableFTransf(context)
754 return {"FINISHED"}
757 # Other Operations -------------------------------------------------------
759 # Remove a Brush or a Canvas
760 class BTool_Remove(Operator):
761 bl_idname = "btool.remove"
762 bl_label = "Bool Tool Remove"
763 bl_description = "Removes all BoolTool config assigned to it"
764 bl_options = {"UNDO"}
766 thisObj: StringProperty("")
767 Prop: StringProperty("")
769 @classmethod
770 def poll(cls, context):
771 return context.active_object is not None
773 def execute(self, context):
774 Remove(context, self.thisObj, self.Prop)
775 return {"FINISHED"}
778 # Apply All to Canvas
779 class BTool_AllBrushToMesh(Operator):
780 bl_idname = "btool.to_mesh"
781 bl_label = "Apply All Canvas"
782 bl_description = "Apply all brushes of this canvas"
783 bl_options = {"UNDO"}
785 @classmethod
786 def poll(cls, context):
787 return context.active_object is not None
789 def execute(self, context):
790 lists = bpy.context.selected_objects
791 ApplyAll(context, lists)
792 return {"FINISHED"}
795 # Apply This Brush to the Canvas
796 class BTool_BrushToMesh(Operator):
797 bl_idname = "btool.brush_to_mesh"
798 bl_label = "Apply this Brush to Canvas"
799 bl_description = "Apply this brush to the canvas"
800 bl_options = {"UNDO"}
802 @classmethod
803 def poll(cls, context):
805 if isBrush(context.active_object):
806 return True
807 else:
808 return False
810 def execute(self, context):
811 ApplyThisBrush(context, bpy.context.active_object)
812 return {"FINISHED"}
815 # TODO
816 # Apply This Brush To Mesh
819 # ------------------- MENU CLASSES ------------------------------
821 # 3Dview Header Menu
822 class VIEW3D_MT_booltool_menu(Menu):
823 bl_label = "Bool Tool"
824 bl_idname = "VIEW3D_MT_booltool_menu"
826 def draw(self, context):
827 layout = self.layout
829 layout.label(text="Auto Boolean")
830 layout.operator(OBJECT_OT_BoolTool_Auto_Difference.bl_idname, text="Difference", icon="SELECT_SUBTRACT")
831 layout.operator(OBJECT_OT_BoolTool_Auto_Union.bl_idname, text="Union", icon="SELECT_EXTEND")
832 layout.operator(OBJECT_OT_BoolTool_Auto_Intersect.bl_idname, text="Intersect", icon="SELECT_INTERSECT")
833 layout.operator(OBJECT_OT_BoolTool_Auto_Slice.bl_idname, text="Slice", icon="SELECT_DIFFERENCE")
835 layout.separator()
837 layout.label(text="Brush Boolean")
838 layout.operator(BTool_Diff.bl_idname, text="Difference", icon="SELECT_SUBTRACT")
839 layout.operator(BTool_Union.bl_idname, text="Union", icon="SELECT_EXTEND")
840 layout.operator(BTool_Inters.bl_idname, text="Intersect", icon="SELECT_INTERSECT")
841 layout.operator(BTool_Slice.bl_idname, text="Slice", icon="SELECT_DIFFERENCE")
843 if isCanvas(context.active_object):
844 layout.separator()
845 layout.operator(BTool_AllBrushToMesh.bl_idname, icon="MOD_LATTICE", text="Apply All")
846 Rem = layout.operator(BTool_Remove.bl_idname, icon="X", text="Remove All")
847 Rem.thisObj = ""
848 Rem.Prop = "CANVAS"
850 if isBrush(context.active_object):
851 layout.separator()
852 layout.operator(BTool_BrushToMesh.bl_idname, icon="MOD_LATTICE", text="Apply Brush")
853 Rem = layout.operator(BTool_Remove.bl_idname, icon="X", text="Remove Brush")
854 Rem.thisObj = ""
855 Rem.Prop = "BRUSH"
858 def VIEW3D_BoolTool_Menu(self, context):
859 self.layout.menu(VIEW3D_MT_booltool_menu.bl_idname)
862 # ---------------- Toolshelf: Tools ---------------------
865 class VIEW3D_PT_booltool_tools(Panel):
866 bl_category = "objectmode"
867 bl_label = "Bool Tool"
868 bl_space_type = "VIEW_3D"
869 bl_region_type = "UI"
870 bl_context = "objectmode"
871 bl_options = {'DEFAULT_CLOSED'}
873 @classmethod
874 def poll(cls, context):
875 return context.active_object is not None
877 def draw(self, context):
878 layout = self.layout
880 col = layout.column(align=True)
881 col.label(text="Auto Boolean")
882 col.operator(OBJECT_OT_BoolTool_Auto_Difference.bl_idname, text="Difference", icon="SELECT_SUBTRACT")
883 col.operator(OBJECT_OT_BoolTool_Auto_Union.bl_idname, text="Union", icon="SELECT_EXTEND")
884 col.operator(OBJECT_OT_BoolTool_Auto_Intersect.bl_idname, text="Intersect", icon="SELECT_INTERSECT")
885 col.operator(OBJECT_OT_BoolTool_Auto_Slice.bl_idname, text="Slice", icon="SELECT_DIFFERENCE")
887 col = layout.column(align=True)
888 col.label(text="Brush Boolean")
889 col.operator(BTool_Diff.bl_idname, text="Difference", icon="SELECT_SUBTRACT")
890 col.operator(BTool_Union.bl_idname, text="Union", icon="SELECT_EXTEND")
891 col.operator(BTool_Inters.bl_idname, text="Intersect", icon="SELECT_INTERSECT")
892 col.operator(BTool_Slice.bl_idname, text="Slice", icon="SELECT_DIFFERENCE")
894 # TODO Draw Poly Brush
895 # main.separator()
897 # col = main.column(align=True)
898 # col.label(text="Draw:", icon="MESH_CUBE")
899 # col.separator()
900 # col.operator(BTool_DrawPolyBrush.bl_idname, icon="LINE_DATA")
903 # ---------- Toolshelf: Properties --------------------------------------------------------
906 class VIEW3D_PT_booltool_config(Panel):
907 bl_category = "objectmode"
908 bl_label = "Properties"
909 bl_space_type = "VIEW_3D"
910 bl_region_type = "UI"
911 bl_context = "objectmode"
912 bl_parent_id = "VIEW3D_PT_booltool_tools"
914 @classmethod
915 def poll(cls, context):
916 actObj = context.active_object
917 return isCanvas(actObj) or isBrush(actObj) # or isPolyBrush(actObj)
919 def draw(self, context):
920 layout = self.layout
921 actObj = context.active_object
923 row = layout.row(align=True)
925 if isCanvas(actObj):
927 row.label(text="CANVAS", icon="MESH_GRID")
928 row = layout.row()
929 row.prop(context.scene, "BoolHide", text="Hide Bool objects")
930 row = layout.row(align=True)
931 row.operator(BTool_AllBrushToMesh.bl_idname, icon="MOD_LATTICE", text="Apply All")
933 row = layout.row(align=True)
934 Rem = row.operator(BTool_Remove.bl_idname, icon="X", text="Remove All")
935 Rem.thisObj = ""
936 Rem.Prop = "CANVAS"
938 if isBrush(actObj):
939 layout.separator()
941 if isBrush(actObj):
943 if actObj["BoolToolBrush"] == "DIFFERENCE":
944 icon = "SELECT_SUBTRACT"
945 elif actObj["BoolToolBrush"] == "UNION":
946 icon = "SELECT_EXTEND"
947 elif actObj["BoolToolBrush"] == "INTERSECT":
948 icon = "SELECT_INTERSECT"
949 elif actObj["BoolToolBrush"] == "SLICE":
950 icon = "SELECT_DIFFERENCE"
952 row.label(text="BRUSH", icon=icon)
954 if actObj["BoolTool_FTransform"] == "True":
955 icon = "PMARKER_ACT"
956 else:
957 icon = "PMARKER"
958 if isFTransf():
959 pass
961 if isFTransf():
962 row = layout.row(align=True)
963 row.operator(BTool_EnableFTransform.bl_idname, text="Fast Vis", icon=icon)
964 row.operator(BTool_EnableThisBrush.bl_idname, text="Enable", icon="HIDE_OFF")
965 else:
966 row.operator(BTool_EnableThisBrush.bl_idname, icon="HIDE_OFF")
968 layout.operator(BTool_BrushToMesh.bl_idname, icon="MOD_LATTICE", text="Apply Brush")
969 Rem = layout.operator(BTool_Remove.bl_idname, icon="X", text="Remove Brush")
970 Rem.thisObj = ""
971 Rem.Prop = "BRUSH"
973 # TODO
974 # if isPolyBrush(actObj):
975 # layout.label(text="POLY BRUSH", icon="LINE_DATA")
976 # mod = actObj.modifiers["BTool_PolyBrush"]
977 # layout.prop(mod, "thickness", text="Size")
980 # ---------- Toolshelf: Brush Viewer -------------------------------------------------------
983 class VIEW3D_PT_booltool_bviewer(Panel):
984 bl_category = "objectmode"
985 bl_label = "Brush Viewer"
986 bl_space_type = "VIEW_3D"
987 bl_region_type = "UI"
988 bl_context = "objectmode"
989 bl_parent_id = "VIEW3D_PT_booltool_tools"
991 @classmethod
992 def poll(cls, context):
993 actObj = bpy.context.active_object
995 if isCanvas(actObj):
996 return True
997 else:
998 return False
1000 def draw(self, context):
1002 actObj = bpy.context.active_object
1004 if isCanvas(actObj):
1006 for mod in actObj.modifiers:
1007 container = self.layout.box()
1008 row = container.row(align=True)
1010 if "BTool_" in mod.name:
1012 if mod.operation == "DIFFERENCE":
1013 icon = "SELECT_SUBTRACT"
1014 elif mod.operation == "UNION":
1015 icon = "SELECT_EXTEND"
1016 elif mod.operation == "INTERSECT":
1017 icon = "SELECT_INTERSECT"
1018 elif mod.operation == "SLICE":
1019 icon = "SELECT_DIFFERENCE"
1021 objSelect = row.operator("btool.find_brush", text=mod.object.name, icon=icon, emboss=False)
1022 objSelect.obj = mod.object.name
1024 EnableIcon = "RESTRICT_VIEW_ON"
1025 if mod.show_viewport:
1026 EnableIcon = "RESTRICT_VIEW_OFF"
1027 Enable = row.operator(BTool_EnableBrush.bl_idname, icon=EnableIcon, emboss=False)
1028 Enable.thisObj = mod.object.name
1030 Remove = row.operator("btool.remove", text="", icon="X", emboss=False)
1031 Remove.thisObj = mod.object.name
1032 Remove.Prop = "THIS"
1034 else:
1035 row.label(text=mod.name)
1037 Up = row.operator("btool.move_stack", icon="TRIA_UP", emboss=False)
1038 Up.modif = mod.name
1039 Up.direction = "UP"
1041 Dw = row.operator("btool.move_stack", icon="TRIA_DOWN", emboss=False)
1042 Dw.modif = mod.name
1043 Dw.direction = "DOWN"
1046 # ------------------ BOOL TOOL ADD-ON PREFERENCES ----------------------------
1049 shortcut_list = (
1050 ("3D View", None),
1051 ("Menu", "Ctrl Shift B"),
1053 ("Auto Operators", None),
1054 ("Difference", "Ctrl Shift Num -"),
1055 ("Union", "Ctrl Shift Num +"),
1056 ("Intersect", "Ctrl Shift Num *"),
1057 ("Slice", "Ctrl Shift Num /"),
1059 ("Brush Operators", None),
1060 ("Difference", "Ctrl Num -"),
1061 ("Union", "Ctrl Num +"),
1062 ("Intersect", "Ctrl Num *"),
1063 ("Slice", "Ctrl Num /"),
1064 ("Brush To Mesh", "Ctrl Num Enter"),
1065 ("All Brushes To Mesh", "Ctrl Shift Num Enter"),
1069 def UpdateBoolTool_Pref(self, context):
1070 if self.fast_transform:
1071 RegisterFastT()
1072 else:
1073 UnRegisterFastT()
1076 # Define Panel classes for updating
1077 panels = (
1078 VIEW3D_PT_booltool_tools,
1079 VIEW3D_PT_booltool_config,
1080 VIEW3D_PT_booltool_bviewer,
1084 def update_panels(self, context):
1085 try:
1086 for panel in panels:
1087 if "bl_rna" in panel.__dict__:
1088 bpy.utils.unregister_class(panel)
1090 for panel in panels:
1091 panel.bl_category = context.preferences.addons[
1092 __name__
1093 ].preferences.category
1094 bpy.utils.register_class(panel)
1096 except Exception as e:
1097 message = "Bool Tool: Updating Panel locations has failed"
1098 print("\n[{}]\n{}\n\nError:\n{}".format(__name__, message, e))
1101 def icon_tria(prop):
1102 if prop:
1103 return "TRIA_DOWN"
1104 return "TRIA_RIGHT"
1107 class PREFS_BoolTool_Props(AddonPreferences):
1108 bl_idname = __name__
1110 fast_transform: BoolProperty(
1111 name="Fast Transformations",
1112 update=UpdateBoolTool_Pref,
1113 description="Replace the Transform HotKeys (G,R,S)\n"
1114 "for a custom version that can optimize the visualization of Brushes",
1116 use_wire: BoolProperty(
1117 name="Display As Wirewrame",
1118 description="Display brush as wireframe instead of bounding box",
1120 category: StringProperty(
1121 name="Tab Name",
1122 description="Set sidebar tab name",
1123 default="Edit",
1124 update=update_panels,
1126 show_shortcuts: BoolProperty(name="Shortcuts")
1128 def draw(self, context):
1129 layout = self.layout
1130 layout.use_property_split = True
1131 layout.use_property_decorate = False
1133 col = layout.column()
1134 col.prop(self, "category")
1135 col.prop(self, "fast_transform")
1136 col.prop(self, "use_wire")
1138 col = layout.column()
1139 col.scale_y = 1.2
1140 col.use_property_split = False
1141 col.prop(self, "show_shortcuts", icon=icon_tria(self.show_shortcuts))
1143 if self.show_shortcuts:
1145 col = layout.column()
1147 for key_name, key_comb in shortcut_list:
1148 if key_comb is None:
1149 col.separator()
1150 col.label(text=key_name)
1151 else:
1152 row = col.row(align=True)
1153 row.scale_y = 0.7
1154 row.box().label(text=key_name)
1155 row.box().label(text=key_comb)
1158 # ------------------- Class List ------------------------------------------------
1160 classes = (
1161 PREFS_BoolTool_Props,
1162 VIEW3D_MT_booltool_menu,
1163 VIEW3D_PT_booltool_tools,
1164 VIEW3D_PT_booltool_config,
1165 VIEW3D_PT_booltool_bviewer,
1166 OBJECT_OT_BoolTool_Auto_Union,
1167 OBJECT_OT_BoolTool_Auto_Difference,
1168 OBJECT_OT_BoolTool_Auto_Intersect,
1169 OBJECT_OT_BoolTool_Auto_Slice,
1170 BTool_Union,
1171 BTool_Diff,
1172 BTool_Inters,
1173 BTool_Slice,
1174 # TODO Draw Poly Brush
1175 # BTool_DrawPolyBrush,
1176 BTool_Remove,
1177 BTool_AllBrushToMesh,
1178 BTool_BrushToMesh,
1179 BTool_FindBrush,
1180 BTool_MoveStack,
1181 BTool_EnableBrush,
1182 BTool_EnableThisBrush,
1183 BTool_EnableFTransform,
1184 BTool_FastTransform,
1188 # ------------------- REGISTER ------------------------------------------------
1190 addon_keymaps = []
1191 addon_keymapsFastT = []
1194 # Fast Transform HotKeys Register
1195 def RegisterFastT():
1196 wm = bpy.context.window_manager
1197 km = wm.keyconfigs.addon.keymaps.new(name="Object Mode", space_type="EMPTY")
1199 kmi = km.keymap_items.new(BTool_FastTransform.bl_idname, "G", "PRESS")
1200 kmi.properties.operator = "Translate"
1201 addon_keymapsFastT.append((km, kmi))
1203 kmi = km.keymap_items.new(BTool_FastTransform.bl_idname, "R", "PRESS")
1204 kmi.properties.operator = "Rotate"
1205 addon_keymapsFastT.append((km, kmi))
1207 kmi = km.keymap_items.new(BTool_FastTransform.bl_idname, "S", "PRESS")
1208 kmi.properties.operator = "Scale"
1209 addon_keymapsFastT.append((km, kmi))
1212 # Fast Transform HotKeys UnRegister
1213 def UnRegisterFastT():
1214 wm = bpy.context.window_manager
1215 kc = wm.keyconfigs.addon
1216 if kc:
1217 for km, kmi in addon_keymapsFastT:
1218 km.keymap_items.remove(kmi)
1220 addon_keymapsFastT.clear()
1223 def register():
1224 for cls in classes:
1225 bpy.utils.register_class(cls)
1226 update_panels(None, bpy.context)
1228 # Scene variables
1229 bpy.types.Scene.BoolHide = BoolProperty(
1230 default=False,
1231 description="Hide boolean objects",
1232 update=update_BoolHide,
1234 bpy.types.VIEW3D_MT_object.append(VIEW3D_BoolTool_Menu)
1236 wm = bpy.context.window_manager
1237 kc = wm.keyconfigs.addon
1239 # create the boolean menu hotkey
1240 if kc is not None:
1241 km = kc.keymaps.new(name="Object Mode")
1243 kmi = km.keymap_items.new("wm.call_menu", "B", "PRESS", ctrl=True, shift=True)
1244 kmi.properties.name = "VIEW3D_MT_booltool_menu"
1245 addon_keymaps.append((km, kmi))
1247 # Brush Operators
1248 kmi = km.keymap_items.new(BTool_Union.bl_idname, "NUMPAD_PLUS", "PRESS", ctrl=True)
1249 addon_keymaps.append((km, kmi))
1250 kmi = km.keymap_items.new(BTool_Diff.bl_idname, "NUMPAD_MINUS", "PRESS", ctrl=True)
1251 addon_keymaps.append((km, kmi))
1252 kmi = km.keymap_items.new(BTool_Inters.bl_idname, "NUMPAD_ASTERIX", "PRESS", ctrl=True)
1253 addon_keymaps.append((km, kmi))
1254 kmi = km.keymap_items.new(BTool_Slice.bl_idname, "NUMPAD_SLASH", "PRESS", ctrl=True)
1255 addon_keymaps.append((km, kmi))
1256 kmi = km.keymap_items.new(BTool_BrushToMesh.bl_idname, "NUMPAD_ENTER", "PRESS", ctrl=True)
1257 addon_keymaps.append((km, kmi))
1258 kmi = km.keymap_items.new(
1259 BTool_AllBrushToMesh.bl_idname,
1260 "NUMPAD_ENTER",
1261 "PRESS",
1262 ctrl=True,
1263 shift=True,
1265 addon_keymaps.append((km, kmi))
1267 # Auto Operators
1268 kmi = km.keymap_items.new(
1269 OBJECT_OT_BoolTool_Auto_Union.bl_idname,
1270 "NUMPAD_PLUS",
1271 "PRESS",
1272 ctrl=True,
1273 shift=True,
1275 addon_keymaps.append((km, kmi))
1276 kmi = km.keymap_items.new(
1277 OBJECT_OT_BoolTool_Auto_Difference.bl_idname,
1278 "NUMPAD_MINUS",
1279 "PRESS",
1280 ctrl=True,
1281 shift=True,
1283 addon_keymaps.append((km, kmi))
1284 kmi = km.keymap_items.new(
1285 OBJECT_OT_BoolTool_Auto_Intersect.bl_idname,
1286 "NUMPAD_ASTERIX",
1287 "PRESS",
1288 ctrl=True,
1289 shift=True,
1291 addon_keymaps.append((km, kmi))
1292 kmi = km.keymap_items.new(
1293 OBJECT_OT_BoolTool_Auto_Slice.bl_idname,
1294 "NUMPAD_SLASH",
1295 "PRESS",
1296 ctrl=True,
1297 shift=True,
1299 addon_keymaps.append((km, kmi))
1302 def unregister():
1303 # Keymapping
1304 # remove keymaps when add-on is deactivated
1305 wm = bpy.context.window_manager
1306 kc = wm.keyconfigs.addon
1307 if kc is not None:
1308 for km, kmi in addon_keymaps:
1309 km.keymap_items.remove(kmi)
1311 addon_keymaps.clear()
1312 UnRegisterFastT()
1313 bpy.types.VIEW3D_MT_object.remove(VIEW3D_BoolTool_Menu)
1314 del bpy.types.Scene.BoolHide
1316 for cls in classes:
1317 bpy.utils.unregister_class(cls)
1320 if __name__ == "__main__":
1321 register()