Fix T70724: PLY import fails with strings
[blender-addons.git] / object_boolean_tools.py
blob642bfff2219f16817a3dda1be7f8cfb39beb5b9c
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 "category": "Object",
31 import bpy
32 from bpy.types import (
33 AddonPreferences,
34 Operator,
35 Panel,
36 Menu,
38 from bpy.props import (
39 BoolProperty,
40 StringProperty,
44 # ------------------- Bool Tool FUNCTIONS -------------------------
45 # Utils:
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
53 for o in objs:
54 o.hide_viewport = hide_state
57 def isCanvas(_obj):
58 try:
59 if _obj["BoolToolRoot"]:
60 return True
61 except:
62 return False
65 def isBrush(_obj):
66 try:
67 if _obj["BoolToolBrush"]:
68 return True
69 except:
70 return False
73 # TODO
74 # def isPolyBrush(_obj):
75 # try:
76 # if _obj["BoolToolPolyBrush"]:
77 # return True
78 # except:
79 # return False
82 def cycles_visibility_set(ob, value=False):
83 if not hasattr(ob, "cycles_visibility"):
84 return
86 vis = ob.cycles_visibility
88 vis.camera = value
89 vis.diffuse = value
90 vis.glossy = value
91 vis.shadow = value
92 vis.transmission = value
93 vis.scatter = value
96 def BT_ObjectByName(obj):
97 for ob in bpy.context.view_layer.objects:
98 if isCanvas(ob) or isBrush(ob):
99 if ob.name == obj:
100 return ob
103 def FindCanvas(obj):
104 for ob in bpy.context.view_layer.objects:
105 if isCanvas(ob):
106 for mod in ob.modifiers:
107 if "BTool_" in mod.name:
108 if obj.name in mod.name:
109 return ob
112 def isFTransf():
113 preferences = bpy.context.preferences
114 addons = preferences.addons
115 addon_prefs = addons[__name__].preferences
116 if addon_prefs.fast_transform:
117 return True
118 else:
119 return False
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:
135 if (
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
144 if useWire:
145 selObj.display_type = "WIRE"
146 else:
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)
159 if is_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"
172 else:
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
185 # Restore the Brush
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)
201 if Prop == "THIS":
202 RemoveThis(thisObj_name)
204 # If the remove was called from the Properties:
205 else:
206 # Remove the Brush Property
207 if Prop == "BRUSH":
208 Canvas = FindCanvas(actObj)
210 if Canvas:
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)
220 if Prop == "CANVAS":
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):
228 for obj in objList:
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
235 else:
236 mod.show_viewport = True
237 mod.show_render = True
240 # Find the Canvas and Enable this Brush
241 def EnableThisBrush(context, set):
242 canvas = None
243 for obj in bpy.context.view_layer.objects:
244 if obj != bpy.context.active_object:
245 if isCanvas(obj):
246 for mod in obj.modifiers:
247 if "BTool_" in mod.name:
248 if mod.object == bpy.context.active_object:
249 canvas = obj
251 for mod in canvas.modifiers:
252 if "BTool_" in mod.name:
253 if mod.object == bpy.context.active_object:
254 if set == "None":
255 if mod.show_viewport:
256 mod.show_viewport = False
257 mod.show_render = False
258 else:
259 mod.show_viewport = True
260 mod.show_render = True
261 else:
262 if set == "True":
263 mod.show_viewport = True
264 else:
265 mod.show_viewport = False
266 return
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"
275 else:
276 actObj["BoolTool_FTransform"] = "True"
277 return
280 # Apply All Brushes to the Canvas
281 def ApplyAll(context, list):
282 objDeleteList = []
283 for selObj in 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)
288 try:
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:
296 if isCanvas(obj):
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:
304 obj.select_set(True)
305 bpy.ops.object.delete()
308 # Apply This Brush to the Canvas
309 def ApplyThisBrush(context, brush):
310 for obj in context.scene.objects:
311 if isCanvas(obj):
312 for mod in obj.modifiers:
313 if "BTool_" + brush.name in mod.name:
314 # Apply This Brush
315 context.view_layer.objects.active = obj
316 try:
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")
324 # Garbage Collector
325 brush.select_set(True)
326 # bpy.ops.object.delete()
329 # ------------------ Bool Tool OPERATORS --------------------------------------
331 # TODO
332 # class BTool_DrawPolyBrush(Operator):
333 # bl_idname = "btool.draw_polybrush"
334 # bl_label = "Draw Poly Brush"
335 # bl_description = (
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"
340 # count = 0
341 # store_cont_draw = False
343 # @classmethod
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
352 # if start:
353 # self.store_cont_draw = continuous
354 # tool_settings.use_gpencil_continuous_drawing = True
355 # else:
356 # tool_settings.use_gpencil_continuous_drawing = self.store_cont_draw
358 # def modal(self, context, event):
359 # self.count += 1
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)
367 # pass
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"
391 # mod.thickness = 1
392 # mod.offset = 0
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):
416 # if context.object:
417 # self.set_cont_draw(context, start=True)
418 # context.window_manager.modal_handler_add(self)
419 # return {"RUNNING_MODAL"}
420 # else:
421 # self.report({"WARNING"}, "No active object, could not finish")
422 # return {"CANCELLED"}
425 # Fast Transform
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("")
433 count = 0
435 def modal(self, context, event):
436 self.count += 1
437 actObj = bpy.context.active_object
438 useWire = bpy.context.preferences.addons[__name__].preferences.use_wire
439 if self.count == 1:
441 if isBrush(actObj) and actObj["BoolTool_FTransform"] == "True":
442 EnableThisBrush(bpy.context, "False")
443 if useWire:
444 actObj.display_type = "WIRE"
445 else:
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":
456 if isBrush(actObj):
457 EnableThisBrush(bpy.context, "True")
458 actObj.display_type = "WIRE"
459 return {"FINISHED"}
461 if event.type in {"RIGHTMOUSE", "ESC"}:
462 if isBrush(actObj):
463 EnableThisBrush(bpy.context, "True")
464 actObj.display_type = "WIRE"
465 return {"CANCELLED"}
467 return {"RUNNING_MODAL"}
469 def invoke(self, context, event):
470 if context.object:
471 context.window_manager.modal_handler_add(self)
472 return {"RUNNING_MODAL"}
473 else:
474 self.report({"WARNING"}, "No active object, could not finish")
475 return {"CANCELLED"}
478 # ------------------- Bool Tool OPERATOR CLASSES --------------------------------------------------------
481 # Brush operators
482 # --------------------------------------------------------------------------------------
485 class BToolSetup():
487 def execute(self, context):
488 Operation(context, self.mode)
489 return {"FINISHED"}
491 def invoke(self, context, event):
492 if len(context.selected_objects) < 2:
493 self.report({"ERROR"}, "At least two objects must be selected")
494 return {"CANCELLED"}
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"}
505 mode = "UNION"
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"}
514 mode = "INTERSECT"
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"}
523 mode = "DIFFERENCE"
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"}
532 mode = "SLICE"
535 # Auto Boolean operators
536 # --------------------------------------------------------------------------------------
539 class Auto_Boolean:
541 def objects_prepare(self):
542 for ob in bpy.context.selected_objects:
543 if ob.type != "MESH":
544 ob.select_set(False)
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")
567 for ob in obs:
568 self.mesh_selection(ob, "SELECT")
569 self.boolean_mod(obj, ob, self.mode)
571 obj.select_set(True)
573 def boolean_mod(self, obj, ob, mode, ob_delete=True):
574 md = obj.modifiers.new("Auto Boolean", "BOOLEAN")
575 md.show_viewport = False
576 md.operation = mode
577 md.object = ob
579 override = {"object": obj}
580 bpy.ops.object.modifier_apply(override, modifier=md.name)
582 if ob_delete:
583 bpy.data.objects.remove(ob)
585 def execute(self, context):
586 self.objects_prepare()
587 self.boolean_operation()
588 return {"FINISHED"}
590 def invoke(self, context, event):
591 if len(context.selected_objects) < 2:
592 self.report({"ERROR"}, "At least two objects must be selected")
593 return {"CANCELLED"}
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"}
604 mode = "UNION"
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"}
613 mode = "DIFFERENCE"
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"}
622 mode = "INTERSECT"
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)
650 if is_local_view:
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
659 return {"FINISHED"}
662 # Utils Class ---------------------------------------------------------------
664 # Find the Brush Selected in Three View
665 class BTool_FindBrush(Operator):
666 bl_idname = "btool.find_brush"
667 bl_label = ""
668 bl_description = "Find the selected brush"
670 obj: StringProperty("")
672 @classmethod
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)
683 return {"FINISHED"}
686 # Move The Modifier in The Stack Up or Down
687 class BTool_MoveStack(Operator):
688 bl_idname = "btool.move_stack"
689 bl_label = ""
690 bl_description = "Move this Brush Up/Down in the Stack"
692 modif: StringProperty("")
693 direction: StringProperty("")
695 @classmethod
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)
704 return {"FINISHED"}
707 # Enable or Disable a Brush in the Three View
708 class BTool_EnableBrush(Operator):
709 bl_idname = "btool.enable_brush"
710 bl_label = ""
711 bl_description = "Removes all BoolTool config assigned to it"
713 thisObj: StringProperty("")
715 @classmethod
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)
722 return {"FINISHED"}
725 # Enable or Disable a Brush Directly
726 class BTool_EnableThisBrush(Operator):
727 bl_idname = "btool.enable_this_brush"
728 bl_label = ""
729 bl_description = "Toggles this brush"
731 @classmethod
732 def poll(cls, context):
733 return context.active_object is not None
735 def execute(self, context):
736 EnableThisBrush(context, "None")
737 return {"FINISHED"}
740 # Enable or Disable a Brush Directly
741 class BTool_EnableFTransform(Operator):
742 bl_idname = "btool.enable_ftransf"
743 bl_label = ""
744 bl_description = "Use Fast Transformations to improve speed"
746 @classmethod
747 def poll(cls, context):
748 return context.active_object is not None
750 def execute(self, context):
751 EnableFTransf(context)
752 return {"FINISHED"}
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("")
767 @classmethod
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)
773 return {"FINISHED"}
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"}
783 @classmethod
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)
790 return {"FINISHED"}
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"}
800 @classmethod
801 def poll(cls, context):
803 if isBrush(context.active_object):
804 return True
805 else:
806 return False
808 def execute(self, context):
809 ApplyThisBrush(context, bpy.context.active_object)
810 return {"FINISHED"}
813 # TODO
814 # Apply This Brush To Mesh
817 # ------------------- MENU CLASSES ------------------------------
819 # 3Dview Header Menu
820 class VIEW3D_MT_booltool_menu(Menu):
821 bl_label = "Bool Tool"
822 bl_idname = "VIEW3D_MT_booltool_menu"
824 def draw(self, context):
825 layout = self.layout
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")
833 layout.separator()
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):
842 layout.separator()
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")
845 Rem.thisObj = ""
846 Rem.Prop = "CANVAS"
848 if isBrush(context.active_object):
849 layout.separator()
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")
852 Rem.thisObj = ""
853 Rem.Prop = "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'}
871 @classmethod
872 def poll(cls, context):
873 return context.active_object is not None
875 def draw(self, context):
876 layout = self.layout
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
893 # main.separator()
895 # col = main.column(align=True)
896 # col.label(text="Draw:", icon="MESH_CUBE")
897 # col.separator()
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"
912 @classmethod
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):
918 layout = self.layout
919 actObj = context.active_object
921 row = layout.row(align=True)
923 if isCanvas(actObj):
925 row.label(text="CANVAS", icon="MESH_GRID")
926 row = layout.row()
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")
933 Rem.thisObj = ""
934 Rem.Prop = "CANVAS"
936 if isBrush(actObj):
937 layout.separator()
939 if isBrush(actObj):
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":
953 icon = "PMARKER_ACT"
954 else:
955 icon = "PMARKER"
956 if isFTransf():
957 pass
959 if isFTransf():
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")
963 else:
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")
968 Rem.thisObj = ""
969 Rem.Prop = "BRUSH"
971 # TODO
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"
989 @classmethod
990 def poll(cls, context):
991 actObj = bpy.context.active_object
993 if isCanvas(actObj):
994 return True
995 else:
996 return False
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"
1032 else:
1033 row.label(text=mod.name)
1035 Up = row.operator("btool.move_stack", icon="TRIA_UP", emboss=False)
1036 Up.modif = mod.name
1037 Up.direction = "UP"
1039 Dw = row.operator("btool.move_stack", icon="TRIA_DOWN", emboss=False)
1040 Dw.modif = mod.name
1041 Dw.direction = "DOWN"
1044 # ------------------ BOOL TOOL ADD-ON PREFERENCES ----------------------------
1047 shortcut_list = (
1048 ("3D View", None),
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:
1069 RegisterFastT()
1070 else:
1071 UnRegisterFastT()
1074 # Define Panel classes for updating
1075 panels = (
1076 VIEW3D_PT_booltool_tools,
1077 VIEW3D_PT_booltool_config,
1078 VIEW3D_PT_booltool_bviewer,
1082 def update_panels(self, context):
1083 try:
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[
1090 __name__
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):
1100 if prop:
1101 return "TRIA_DOWN"
1102 return "TRIA_RIGHT"
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(
1119 name="Tab Name",
1120 description="Set sidebar tab name",
1121 default="Edit",
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()
1137 col.scale_y = 1.2
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:
1147 col.separator()
1148 col.label(text=key_name)
1149 else:
1150 row = col.row(align=True)
1151 row.scale_y = 0.7
1152 row.box().label(text=key_name)
1153 row.box().label(text=key_comb)
1156 # ------------------- Class List ------------------------------------------------
1158 classes = (
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,
1168 BTool_Union,
1169 BTool_Diff,
1170 BTool_Inters,
1171 BTool_Slice,
1172 # TODO Draw Poly Brush
1173 # BTool_DrawPolyBrush,
1174 BTool_Remove,
1175 BTool_AllBrushToMesh,
1176 BTool_BrushToMesh,
1177 BTool_FindBrush,
1178 BTool_MoveStack,
1179 BTool_EnableBrush,
1180 BTool_EnableThisBrush,
1181 BTool_EnableFTransform,
1182 BTool_FastTransform,
1186 # ------------------- REGISTER ------------------------------------------------
1188 addon_keymaps = []
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
1214 if kc:
1215 for km, kmi in addon_keymapsFastT:
1216 km.keymap_items.remove(kmi)
1218 addon_keymapsFastT.clear()
1221 def register():
1222 for cls in classes:
1223 bpy.utils.register_class(cls)
1224 update_panels(None, bpy.context)
1226 # Scene variables
1227 bpy.types.Scene.BoolHide = BoolProperty(
1228 default=False,
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
1238 if kc is not None:
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))
1245 # Brush Operators
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,
1258 "NUMPAD_ENTER",
1259 "PRESS",
1260 ctrl=True,
1261 shift=True,
1263 addon_keymaps.append((km, kmi))
1265 # Auto Operators
1266 kmi = km.keymap_items.new(
1267 OBJECT_OT_BoolTool_Auto_Union.bl_idname,
1268 "NUMPAD_PLUS",
1269 "PRESS",
1270 ctrl=True,
1271 shift=True,
1273 addon_keymaps.append((km, kmi))
1274 kmi = km.keymap_items.new(
1275 OBJECT_OT_BoolTool_Auto_Difference.bl_idname,
1276 "NUMPAD_MINUS",
1277 "PRESS",
1278 ctrl=True,
1279 shift=True,
1281 addon_keymaps.append((km, kmi))
1282 kmi = km.keymap_items.new(
1283 OBJECT_OT_BoolTool_Auto_Intersect.bl_idname,
1284 "NUMPAD_ASTERIX",
1285 "PRESS",
1286 ctrl=True,
1287 shift=True,
1289 addon_keymaps.append((km, kmi))
1290 kmi = km.keymap_items.new(
1291 OBJECT_OT_BoolTool_Auto_Slice.bl_idname,
1292 "NUMPAD_SLASH",
1293 "PRESS",
1294 ctrl=True,
1295 shift=True,
1297 addon_keymaps.append((km, kmi))
1300 def unregister():
1301 # Keymapping
1302 # remove keymaps when add-on is deactivated
1303 wm = bpy.context.window_manager
1304 kc = wm.keyconfigs.addon
1305 if kc is not None:
1306 for km, kmi in addon_keymaps:
1307 km.keymap_items.remove(kmi)
1309 addon_keymaps.clear()
1310 UnRegisterFastT()
1311 bpy.types.VIEW3D_MT_object.remove(VIEW3D_BoolTool_Menu)
1312 del bpy.types.Scene.BoolHide
1314 for cls in classes:
1315 bpy.utils.unregister_class(cls)
1318 if __name__ == "__main__":
1319 register()