Fix version check for PLY
[blender-addons.git] / object_boolean_tools.py
blob44362ad176d9c94d7d8be3dee0fed19bc299bba9
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",
24 "version": (0, 3, 6),
25 "blender": (2, 78, 0),
26 "location": "View3D > Toolshelf",
27 "description": "Bool Tools Hotkey: Ctrl Shift B",
28 "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Object/BoolTool",
29 "tracker_url": "https://developer.blender.org/maniphest/task/edit/form/2/",
30 "category": "Object",
33 import bpy
34 from bpy.app.handlers import persistent
35 from bpy.types import (
36 AddonPreferences,
37 Operator,
38 Panel,
39 Menu,
41 from bpy.props import (
42 BoolProperty,
43 StringProperty,
44 EnumProperty,
48 # ------------------- Bool Tool FUNCTIONS------------------------------
49 # Utils:
51 # Hide boolean objects
52 def update_BoolHide(self, context):
53 ao = context.scene.objects.active
54 objs = [i.object for i in ao.modifiers if i.type == 'BOOLEAN']
55 hide_state = context.scene.BoolHide
57 for o in objs:
58 o.hide = hide_state
61 # Object is a Canvas
62 def isCanvas(_obj):
63 try:
64 if _obj["BoolToolRoot"]:
65 return True
66 except:
67 return False
70 # Object is a Brush Tool Bool
71 def isBrush(_obj):
72 try:
73 if _obj["BoolToolBrush"]:
74 return True
75 except:
76 return False
79 # Object is a Poly Brush Tool Bool collection
80 def isPolyBrush(_obj):
81 try:
82 if _obj["BoolToolPolyBrush"]:
83 return True
84 except:
85 return False
88 def BT_ObjectByName(obj):
89 for ob in bpy.context.scene.objects:
90 if isCanvas(ob) or isBrush(ob):
91 if ob.name == obj:
92 return ob
95 def FindCanvas(obj):
96 for ob in bpy.context.scene.objects:
97 if isCanvas(ob):
98 for mod in ob.modifiers:
99 if ("BTool_" in mod.name):
100 if (obj.name in mod.name):
101 return ob
104 def isFTransf():
105 user_preferences = bpy.context.user_preferences
106 addons = user_preferences.addons
107 addon_prefs = addons[__name__].preferences
108 if addon_prefs.fast_transform:
109 return True
110 else:
111 return False
115 # EXPERIMENTAL FEATURES
116 def isMakeVertexGroup():
117 user_preferences = bpy.context.user_preferences
118 addon_prefs = user_preferences.addons[__name__].preferences
119 if addon_prefs.make_vertex_groups:
120 return True
121 else:
122 return False
124 def isMakeBoundary():
125 user_preferences = bpy.context.user_preferences
126 addon_prefs = user_preferences.addons[__name__].preferences
127 if addon_prefs.make_boundary:
128 return True
129 else:
130 return False
134 def ConvertToMesh(obj):
135 act = bpy.context.scene.objects.active
136 bpy.context.scene.objects.active = obj
137 bpy.ops.object.convert(target="MESH")
138 bpy.context.scene.objects.active = act
141 # Do the Union, Difference and Intersection Operations with a Brush
142 def Operation(context, _operation):
144 prefs = bpy.context.user_preferences.addons[__name__].preferences
145 useWire = prefs.use_wire
146 solver = prefs.solver
148 for selObj in bpy.context.selected_objects:
149 if selObj != context.active_object and (selObj.type == "MESH" or selObj.type == "CURVE"):
150 if selObj.type == "CURVE":
151 ConvertToMesh(selObj)
152 actObj = context.active_object
153 selObj.hide_render = True
154 cyclesVis = selObj.cycles_visibility
155 # for obj in bpy.context.scene.objects:
156 # if isCanvas(obj):
157 # for mod in obj.modifiers:
158 # if(mod.name == "BTool_" + selObj.name):
159 # obj.modifiers.remove(mod)
161 if useWire:
162 selObj.draw_type = "WIRE"
163 else:
164 selObj.draw_type = "BOUNDS"
166 cyclesVis.camera = False
167 cyclesVis.diffuse = False
168 cyclesVis.glossy = False
169 cyclesVis.shadow = False
170 cyclesVis.transmission = False
171 if _operation == "SLICE":
172 # copies dupli_group property(empty), but group property is empty (users_group = None)
173 clone = context.active_object.copy()
174 # clone.select=True
175 context.scene.objects.link(clone)
176 sliceMod = clone.modifiers.new("BTool_" + selObj.name, "BOOLEAN") # add mod to clone obj
177 sliceMod.object = selObj
178 sliceMod.operation = "DIFFERENCE"
179 clone["BoolToolRoot"] = True
180 newMod = actObj.modifiers.new("BTool_" + selObj.name, "BOOLEAN")
181 newMod.object = selObj
182 newMod.solver = solver
183 if _operation == "SLICE":
184 newMod.operation = "INTERSECT"
185 else:
186 newMod.operation = _operation
188 actObj["BoolToolRoot"] = True
189 selObj["BoolToolBrush"] = _operation
190 selObj["BoolTool_FTransform"] = "False"
193 # Remove Obejcts form the BoolTool System
194 def Remove(context, thisObj_name, Prop):
195 # Find the Brush pointed in the Tree View and Restore it, active is the Canvas
196 actObj = context.active_object
198 # Restore the Brush
199 def RemoveThis(_thisObj_name):
200 for obj in bpy.context.scene.objects:
201 # if it's the brush object
202 if obj.name == _thisObj_name:
203 cyclesVis = obj.cycles_visibility
204 obj.draw_type = "TEXTURED"
205 del obj["BoolToolBrush"]
206 del obj["BoolTool_FTransform"]
207 cyclesVis.camera = True
208 cyclesVis.diffuse = True
209 cyclesVis.glossy = True
210 cyclesVis.shadow = True
211 cyclesVis.transmission = True
213 # Remove it from the Canvas
214 for mod in actObj.modifiers:
215 if ("BTool_" in mod.name):
216 if (_thisObj_name in mod.name):
217 actObj.modifiers.remove(mod)
219 if Prop == "THIS":
220 RemoveThis(thisObj_name)
222 # If the remove was called from the Properties:
223 else:
224 # Remove the Brush Property
225 if Prop == "BRUSH":
226 Canvas = FindCanvas(actObj)
227 if Canvas:
228 for mod in Canvas.modifiers:
229 if ("BTool_" in mod.name):
230 if (actObj.name in mod.name):
231 Canvas.modifiers.remove(mod)
232 cyclesVis = actObj.cycles_visibility
233 actObj.draw_type = "TEXTURED"
234 del actObj["BoolToolBrush"]
235 del actObj["BoolTool_FTransform"]
236 cyclesVis.camera = True
237 cyclesVis.diffuse = True
238 cyclesVis.glossy = True
239 cyclesVis.shadow = True
240 cyclesVis.transmission = True
242 if Prop == "CANVAS":
243 for mod in actObj.modifiers:
244 if ("BTool_" in mod.name):
245 RemoveThis(mod.object.name)
248 # Tooble the Enable the Brush Object Propertie
249 def EnableBrush(context, objList, canvas):
250 for obj in objList:
251 for mod in canvas.modifiers:
252 if ("BTool_" in mod.name and mod.object.name == obj):
254 if (mod.show_viewport):
255 mod.show_viewport = False
256 mod.show_render = False
257 else:
258 mod.show_viewport = True
259 mod.show_render = True
262 # Find the Canvas and Enable this Brush
263 def EnableThisBrush(context, set):
264 canvas = None
265 for obj in bpy.context.scene.objects:
266 if obj != bpy.context.active_object:
267 if isCanvas(obj):
268 for mod in obj.modifiers:
269 if ("BTool_" in mod.name):
270 if mod.object == bpy.context.active_object:
271 canvas = obj
273 for mod in canvas.modifiers:
274 if ("BTool_" in mod.name):
275 if mod.object == bpy.context.active_object:
276 if set == "None":
277 if (mod.show_viewport):
278 mod.show_viewport = False
279 mod.show_render = False
280 else:
281 mod.show_viewport = True
282 mod.show_render = True
283 else:
284 if (set == "True"):
285 mod.show_viewport = True
286 else:
287 mod.show_viewport = False
288 return
291 # Tooble the Fast Transform Propertie of the Active Brush
292 def EnableFTransf(context):
293 actObj = bpy.context.active_object
295 if actObj["BoolTool_FTransform"] == "True":
296 actObj["BoolTool_FTransform"] = "False"
297 else:
298 actObj["BoolTool_FTransform"] = "True"
299 return
302 # Apply All Brushes to the Canvas
303 def ApplyAll(context, list):
304 objDeleteList = []
305 for selObj in list:
306 if isCanvas(selObj) and selObj == context.active_object:
307 for mod in selObj.modifiers:
308 if ("BTool_" in mod.name):
309 objDeleteList.append(mod.object)
310 try:
311 bpy.ops.object.modifier_apply(modifier=mod.name)
312 except: # if fails the means it is multiuser data
313 context.active_object.data = context.active_object.data.copy() # so just make data unique
314 bpy.ops.object.modifier_apply(modifier=mod.name)
315 del selObj['BoolToolRoot']
317 for obj in context.scene.objects:
318 if isCanvas(obj):
319 for mod in obj.modifiers:
320 if mod.type == 'BOOLEAN':
321 if mod.object in objDeleteList: # do not delete brush that is used by another canvas
322 objDeleteList.remove(mod.object) # remove it from deletion
324 bpy.ops.object.select_all(action='DESELECT')
325 for obj in objDeleteList:
326 obj.select = True
327 bpy.ops.object.delete()
330 # Apply This Brush to the Canvas
331 def ApplyThisBrush(context, brush):
332 for obj in context.scene.objects:
333 if isCanvas(obj):
334 for mod in obj.modifiers:
335 if ("BTool_" + brush.name in mod.name):
337 # EXPERIMENTAL
338 if isMakeVertexGroup():
339 # Turn all faces of the Brush selected
340 bpy.context.scene.objects.active = brush
341 bpy.ops.object.mode_set(mode='EDIT')
342 bpy.ops.mesh.select_all(action='SELECT')
343 bpy.ops.object.mode_set(mode='OBJECT')
345 # Turn off al faces of the Canvas selected
346 bpy.context.scene.objects.active = canvas
347 bpy.ops.object.mode_set(mode='EDIT')
348 bpy.ops.mesh.select_all(action='DESELECT')
349 bpy.ops.object.mode_set(mode='OBJECT')
352 # Apply This Brush
353 context.scene.objects.active = obj
354 try:
355 bpy.ops.object.modifier_apply(modifier=mod.name)
356 except: # if fails the means it is multiuser data
357 context.active_object.data = context.active_object.data.copy() # so just make data unique
358 bpy.ops.object.modifier_apply(modifier=mod.name)
359 bpy.ops.object.select_all(action='TOGGLE')
360 bpy.ops.object.select_all(action='DESELECT')
363 # EXPERIMENTAL
364 if isMakeVertexGroup():
365 # Make Vertex Group
366 bpy.ops.object.mode_set(mode='EDIT')
367 bpy.ops.object.vertex_group_assign_new()
368 bpy.ops.mesh.select_all(action='DESELECT')
369 bpy.ops.object.mode_set(mode='OBJECT')
371 canvas.vertex_groups.active.name = "BTool_" + brush.name
374 # Garbage Colletor
375 brush.select = True
376 # bpy.ops.object.delete()
379 def GCollector(_obj):
380 if isCanvas(_obj):
381 BTRoot = False
382 for mod in _obj.modifiers:
383 if ("BTool_" in mod.name):
384 BTRoot = True
385 if mod.object is None:
386 _obj.modifiers.remove(mod)
387 if not BTRoot:
388 del _obj["BoolToolRoot"]
391 # Handle the callbacks when modifing things in the scene
392 @persistent
393 def HandleScene(scene):
394 if bpy.data.objects.is_updated:
395 for ob in bpy.data.objects:
396 if ob.is_updated:
397 GCollector(ob)
400 # ------------------ Bool Tool OPERATORS-----------------------------------------------------
402 class BTool_DrawPolyBrush(Operator):
403 """Draw Polygonal Mask, can be applyied to Canvas > Brush or Directly. ESC to Exit"""
404 bl_idname = "btool.draw_polybrush"
405 bl_label = "Draw Poly Brush"
407 count = 0
409 @classmethod
410 def poll(cls, context):
411 return context.active_object is not None
413 def modal(self, context, event):
414 self.count += 1
415 actObj = bpy.context.active_object
416 if self.count == 1:
417 actObj.select = True
418 bpy.ops.gpencil.draw('INVOKE_DEFAULT', mode="DRAW_POLY")
420 if event.type in {'RET', 'NUMPAD_ENTER'}:
422 bpy.ops.gpencil.convert(type='POLY')
423 for obj in context.selected_objects:
424 if obj.type == "CURVE":
425 obj.name = "PolyDraw"
426 bpy.context.scene.objects.active = obj
427 bpy.ops.object.select_all(action='DESELECT')
428 obj.select = True
429 bpy.ops.object.convert(target="MESH")
430 bpy.ops.object.mode_set(mode='EDIT')
431 bpy.ops.mesh.select_all(action='SELECT')
432 bpy.ops.mesh.edge_face_add()
433 bpy.ops.mesh.flip_normals()
434 bpy.ops.object.mode_set(mode='OBJECT')
435 bpy.ops.object.origin_set(type='ORIGIN_CENTER_OF_MASS')
436 bpy.ops.object.modifier_add(type="SOLIDIFY")
437 for mod in obj.modifiers:
438 if mod.name == "Solidify":
439 mod.name = "BTool_PolyBrush"
440 mod.thickness = 1
441 mod.offset = 0
442 obj["BoolToolPolyBrush"] = True
444 bpy.ops.object.select_all(action='DESELECT')
445 bpy.context.scene.objects.active = actObj
446 bpy.context.scene.update()
447 actObj.select = True
448 obj.select = True
449 # try:
450 bpy.context.scene.grease_pencil.clear()
451 bpy.ops.gpencil.data_unlink()
453 return {'FINISHED'}
455 if event.type in {'ESC'}:
456 bpy.ops.ed.undo() # remove o Grease Pencil
457 return {'CANCELLED'}
459 return {'RUNNING_MODAL'}
461 def invoke(self, context, event):
462 if context.object:
463 context.window_manager.modal_handler_add(self)
464 return {'RUNNING_MODAL'}
465 else:
466 self.report({'WARNING'}, "No active object, could not finish")
467 return {'CANCELLED'}
470 # Fast Transform
471 class BTool_FastTransform(Operator):
472 """Enable Fast Transform"""
473 bl_idname = "btool.fast_transform"
474 bl_label = "Fast Transform"
476 operator = StringProperty("")
478 count = 0
480 def modal(self, context, event):
481 self.count += 1
482 actObj = bpy.context.active_object
483 useWire = bpy.context.user_preferences.addons[__name__].preferences.use_wire
484 if self.count == 1:
486 if isBrush(actObj) and actObj["BoolTool_FTransform"] == "True":
487 EnableThisBrush(bpy.context, "False")
488 if useWire:
489 actObj.draw_type = "WIRE"
490 else:
491 actObj.draw_type = "BOUNDS"
493 if self.operator == "Translate":
494 bpy.ops.transform.translate('INVOKE_DEFAULT')
495 if self.operator == "Rotate":
496 bpy.ops.transform.rotate('INVOKE_DEFAULT')
497 if self.operator == "Scale":
498 bpy.ops.transform.resize('INVOKE_DEFAULT')
500 if event.type == 'LEFTMOUSE':
501 if isBrush(actObj):
502 EnableThisBrush(bpy.context, "True")
503 actObj.draw_type = "WIRE"
504 return {'FINISHED'}
506 if event.type in {'RIGHTMOUSE', 'ESC'}:
507 if isBrush(actObj):
508 EnableThisBrush(bpy.context, "True")
509 actObj.draw_type = "WIRE"
510 return {'CANCELLED'}
512 return {'RUNNING_MODAL'}
514 def invoke(self, context, event):
515 if context.object:
516 context.window_manager.modal_handler_add(self)
517 return {'RUNNING_MODAL'}
518 else:
519 self.report({'WARNING'}, "No active object, could not finish")
520 return {'CANCELLED'}
523 # ------------------- Bool Tool OPERATOR CLASSES --------------------------------------------------------
525 # Brush Operators --------------------------------------------
527 # Boolean Union Operator
528 class BTool_Union(Operator):
529 """This operator add a union brush to a canvas"""
530 bl_idname = "btool.boolean_union"
531 bl_label = "Brush Union"
533 @classmethod
534 def poll(cls, context):
535 return context.active_object is not None
537 def execute(self, context):
538 Operation(context, "UNION")
539 return {'FINISHED'}
542 # Boolean Intersection Operator
543 class BTool_Inters(Operator):
544 """This operator add a intersect brush to a canvas"""
545 bl_idname = "btool.boolean_inters"
546 bl_label = "Brush Intersection"
548 @classmethod
549 def poll(cls, context):
550 return context.active_object is not None
552 def execute(self, context):
553 Operation(context, "INTERSECT")
554 return {'FINISHED'}
557 # Boolean Difference Operator
558 class BTool_Diff(Operator):
559 """This operator add a difference brush to a canvas"""
560 bl_idname = "btool.boolean_diff"
561 bl_label = "Brush Difference"
563 @classmethod
564 def poll(cls, context):
565 return context.active_object is not None
567 def execute(self, context):
568 Operation(context, "DIFFERENCE")
569 return {'FINISHED'}
572 # Boolean Slices Operator
573 class BTool_Slice(Operator):
574 """This operator add a intersect brush to a canvas"""
575 bl_idname = "btool.boolean_slice"
576 bl_label = "Brush Slice"
578 @classmethod
579 def poll(cls, context):
580 return context.active_object is not None
582 def execute(self, context):
583 Operation(context, "SLICE")
584 return {'FINISHED'}
587 # Auto Boolean operators (maintainer Mikhail Rachinskiy) -------------------------------
589 class AutoBoolean:
591 solver = EnumProperty(
592 name="Boolean Solver",
593 items=(('BMESH', "BMesh", "BMesh solver is faster, but less stable "
594 "and cannot handle coplanar geometry"),
595 ('CARVE', "Carve", "Carve solver is slower, but more stable "
596 "and can handle simple cases of coplanar geometry")),
597 description="Specify solver for boolean operation",
598 options={'SKIP_SAVE'},
601 def __init__(self):
602 self.solver = bpy.context.user_preferences.addons[__name__].preferences.solver
604 def objects_prepare(self):
605 for ob in bpy.context.selected_objects:
606 if ob.type != 'MESH':
607 ob.select = False
608 bpy.ops.object.make_single_user(object=True, obdata=True)
609 bpy.ops.object.convert(target='MESH')
611 def mesh_selection(self, ob, select_action):
612 scene = bpy.context.scene
613 obj = bpy.context.active_object
615 scene.objects.active = ob
616 bpy.ops.object.mode_set(mode='EDIT')
618 bpy.ops.mesh.reveal()
619 bpy.ops.mesh.select_all(action=select_action)
621 bpy.ops.object.mode_set(mode='OBJECT')
622 scene.objects.active = obj
624 def boolean_operation(self):
625 obj = bpy.context.active_object
626 obj.select = False
627 obs = bpy.context.selected_objects
629 self.mesh_selection(obj, 'DESELECT')
630 for ob in obs:
631 self.mesh_selection(ob, 'SELECT')
632 self.boolean_mod(obj, ob, self.mode)
633 obj.select = True
635 def boolean_mod(self, obj, ob, mode, ob_delete=True):
636 md = obj.modifiers.new("Auto Boolean", 'BOOLEAN')
637 md.show_viewport = False
638 md.operation = mode
639 md.solver = self.solver
640 md.object = ob
642 bpy.ops.object.modifier_apply(modifier="Auto Boolean")
643 if not ob_delete:
644 return
645 bpy.context.scene.objects.unlink(ob)
646 bpy.data.objects.remove(ob)
649 class Auto_Union(AutoBoolean, Operator):
650 """Combine selected objects"""
651 bl_idname = "btool.auto_union"
652 bl_label = "Union"
653 bl_options = {'REGISTER', 'UNDO'}
655 mode = 'UNION'
657 def execute(self, context):
658 self.objects_prepare()
659 self.boolean_operation()
660 return {'FINISHED'}
663 class Auto_Difference(AutoBoolean, Operator):
664 """Subtract selected objects from active object"""
665 bl_idname = "btool.auto_difference"
666 bl_label = "Difference"
667 bl_options = {'REGISTER', 'UNDO'}
669 mode = 'DIFFERENCE'
671 def execute(self, context):
672 self.objects_prepare()
673 self.boolean_operation()
674 return {'FINISHED'}
677 class Auto_Intersect(AutoBoolean, Operator):
678 """Keep only intersecting geometry"""
679 bl_idname = "btool.auto_intersect"
680 bl_label = "Intersect"
681 bl_options = {'REGISTER', 'UNDO'}
683 mode = 'INTERSECT'
685 def execute(self, context):
686 self.objects_prepare()
687 self.boolean_operation()
688 return {'FINISHED'}
691 class Auto_Slice(AutoBoolean, Operator):
692 """Slice active object along the selected object (can handle only two objects at a time)"""
693 bl_idname = "btool.auto_slice"
694 bl_label = "Slice"
695 bl_options = {'REGISTER', 'UNDO'}
697 def execute(self, context):
698 self.objects_prepare()
700 scene = context.scene
701 obj = context.active_object
702 obj.select = False
703 ob = context.selected_objects[0]
705 self.mesh_selection(obj, 'DESELECT')
706 self.mesh_selection(ob, 'SELECT')
708 obj_copy = obj.copy()
709 obj_copy.data = obj.data.copy()
710 scene.objects.link(obj_copy)
712 self.boolean_mod(obj, ob, 'DIFFERENCE', ob_delete=False)
713 scene.objects.active = obj_copy
714 self.boolean_mod(obj_copy, ob, 'INTERSECT')
715 obj_copy.select = True
717 return {'FINISHED'}
720 class Auto_Subtract(AutoBoolean, Operator):
721 """Subtract selected object from active object, """ \
722 """subtracted object not removed (can handle only two objects at a time)"""
723 bl_idname = "btool.auto_subtract"
724 bl_label = "Subtract"
725 bl_options = {'REGISTER', 'UNDO'}
727 def execute(self, context):
728 self.objects_prepare()
730 obj = context.active_object
731 obj.select = False
732 ob = context.selected_objects[0]
734 self.mesh_selection(obj, 'DESELECT')
735 self.mesh_selection(ob, 'SELECT')
736 self.boolean_mod(obj, ob, 'DIFFERENCE', ob_delete=False)
738 return {'FINISHED'}
741 # Utils Class ---------------------------------------------------------------
743 # Find the Brush Selected in Three View
744 class BTool_FindBrush(Operator):
745 """Find the this brush"""
746 bl_idname = "btool.find_brush"
747 bl_label = ""
748 obj = StringProperty("")
750 @classmethod
751 def poll(cls, context):
752 return context.active_object is not None
754 def execute(self, context):
755 for ob in bpy.context.scene.objects:
756 if (ob.name == self.obj):
757 bpy.ops.object.select_all(action='TOGGLE')
758 bpy.ops.object.select_all(action='DESELECT')
759 bpy.context.scene.objects.active = ob
760 ob.select = True
761 return {'FINISHED'}
764 # Mode The Modifier in The Stack Up or Down
765 class BTool_MoveStack(Operator):
766 """Move this Brush Up/Down in the Stack"""
767 bl_idname = "btool.move_stack"
768 bl_label = ""
769 modif = StringProperty("")
770 direction = StringProperty("")
772 @classmethod
773 def poll(cls, context):
774 return context.active_object is not None
776 def execute(self, context):
777 if (self.direction == "UP"):
778 bpy.ops.object.modifier_move_up(modifier=self.modif)
779 if (self.direction == "DOWN"):
780 bpy.ops.object.modifier_move_down(modifier=self.modif)
781 return {'FINISHED'}
784 # Enable or Disable a Brush in th Three View
785 class BTool_EnableBrush(Operator):
786 """Removes all BoolTool config assigned to it"""
787 bl_idname = "btool.enable_brush"
788 bl_label = ""
790 thisObj = StringProperty("")
792 @classmethod
793 def poll(cls, context):
794 return context.active_object is not None
796 def execute(self, context):
797 # in this case is just one object but the function accept more than one at once
798 EnableBrush(context, [self.thisObj], context.active_object)
799 return {'FINISHED'}
802 # Enable or Disabel a Brush Directly
803 class BTool_EnableThisBrush(Operator):
804 """ Toggles this brush"""
805 bl_idname = "btool.enable_this_brush"
806 bl_label = ""
808 @classmethod
809 def poll(cls, context):
810 return context.active_object is not None
812 def execute(self, context):
813 EnableThisBrush(context, "None")
814 return {'FINISHED'}
817 # Enable or Disabel a Brush Directly
818 class BTool_EnableFTransform(Operator):
819 """Use Fast Transformations to improve speed"""
820 bl_idname = "btool.enable_ftransf"
821 bl_label = ""
823 @classmethod
824 def poll(cls, context):
825 return context.active_object is not None
827 def execute(self, context):
828 EnableFTransf(context)
829 return {'FINISHED'}
832 # Other Operations -------------------------------------------------------
834 # Remove a Brush or a Canvas
835 class BTool_Remove(Operator):
836 """Removes all BoolTool config assigned to it"""
837 bl_idname = "btool.remove"
838 bl_label = ""
839 bl_options = {'UNDO'}
840 thisObj = StringProperty("")
841 Prop = StringProperty("")
843 @classmethod
844 def poll(cls, context):
845 return context.active_object is not None
847 def execute(self, context):
848 Remove(context, self.thisObj, self.Prop)
849 return {'FINISHED'}
852 # Apply All to Canvas
853 class BTool_AllBrushToMesh(Operator):
854 """Apply all brushes of this canvas"""
855 bl_idname = "btool.to_mesh"
856 bl_label = "Apply All Canvas"
857 bl_options = {'UNDO'}
859 @classmethod
860 def poll(cls, context):
861 return context.active_object is not None
863 def execute(self, context):
864 list = bpy.context.selected_objects
865 ApplyAll(context, list)
866 return {'FINISHED'}
869 # Apply This Brush to the Canvas
870 class BTool_BrushToMesh(Operator):
871 """Apply this brush to the canvas"""
872 bl_idname = "btool.brush_to_mesh"
873 bl_label = "Apply this Brush to Canvas"
874 bl_options = {'UNDO'}
876 @classmethod
877 def poll(cls, context):
879 if isBrush(context.active_object):
880 return True
881 else:
882 return False
884 def execute(self, context):
885 ApplyThisBrush(context, bpy.context.active_object)
886 return {'FINISHED'}
889 # TODO
890 # Apply This Brush To Mesh
893 # ------------------- MENU CLASSES ------------------------------
895 # 3Dview Header Menu
896 class BoolTool_Menu(Menu):
897 bl_label = "BoolTool Operators"
898 bl_idname = "OBJECT_MT_BoolTool_Menu"
900 def draw(self, context):
901 layout = self.layout
903 layout.label("Auto Boolean:")
904 layout.operator(Auto_Difference.bl_idname, icon="ROTACTIVE")
905 layout.operator(Auto_Union.bl_idname, icon="ROTATECOLLECTION")
906 layout.operator(Auto_Intersect.bl_idname, icon="ROTATECENTER")
907 layout.operator(Auto_Slice.bl_idname, icon="ROTATECENTER")
908 layout.operator(Auto_Subtract.bl_idname, icon="ROTACTIVE")
909 layout.separator()
911 layout.label("Brush Boolean:")
912 layout.operator(BTool_Diff.bl_idname, icon="ROTACTIVE")
913 layout.operator(BTool_Union.bl_idname, icon="ROTATECOLLECTION")
914 layout.operator(BTool_Inters.bl_idname, icon="ROTATECENTER")
915 layout.operator(BTool_Slice.bl_idname, icon="ROTATECENTER")
916 layout.separator()
918 if (isCanvas(context.active_object)):
919 layout.separator()
920 layout.operator(BTool_AllBrushToMesh.bl_idname, icon="MOD_LATTICE", text="Apply All")
921 Rem = layout.operator(BTool_Remove.bl_idname, icon="CANCEL", text="Remove All")
922 Rem.thisObj = ""
923 Rem.Prop = "CANVAS"
925 if (isBrush(context.active_object)):
926 layout.separator()
927 layout.operator(BTool_BrushToMesh.bl_idname, icon="MOD_LATTICE", text="Apply Brush")
928 Rem = layout.operator(BTool_Remove.bl_idname, icon="CANCEL", text="Remove Brush")
929 Rem.thisObj = ""
930 Rem.Prop = "BRUSH"
933 def VIEW3D_BoolTool_Menu(self, context):
934 self.layout.menu(BoolTool_Menu.bl_idname)
937 # ---------------- Toolshelf: Tools ---------------------
939 class BoolTool_Tools(Panel):
940 bl_category = "Tools"
941 bl_label = "Bool Tools"
942 bl_idname = "BoolTool_Tools"
943 bl_space_type = "VIEW_3D"
944 bl_region_type = "TOOLS"
945 bl_context = 'objectmode'
947 @classmethod
948 def poll(cls, context):
949 return context.active_object is not None
951 def draw(self, context):
952 layout = self.layout
953 obj = context.active_object
954 obs_len = len(context.selected_objects)
956 row = layout.split(0.7)
957 row.label("Bool Tools:")
958 row.operator("help.bool_tool", text="", icon="QUESTION")
960 main = layout.column(align=True)
961 main.enabled = obj.type == 'MESH' and obs_len > 0
963 main.separator()
965 col = main.column(align=True)
966 col.enabled = obs_len > 1
967 col.label("Auto Boolean:", icon="MODIFIER")
968 col.separator()
969 col.operator(Auto_Difference.bl_idname, icon="ROTACTIVE")
970 col.operator(Auto_Union.bl_idname, icon="ROTATECOLLECTION")
971 col.operator(Auto_Intersect.bl_idname, icon="ROTATECENTER")
973 main.separator()
975 col = main.column(align=True)
976 col.enabled = obs_len == 2
977 col.operator(Auto_Slice.bl_idname, icon="ROTATECENTER")
978 col.operator(Auto_Subtract.bl_idname, icon="ROTACTIVE")
980 main.separator()
982 col = main.column(align=True)
983 col.enabled = obs_len > 1
984 col.label("Brush Boolean:", icon="MODIFIER")
985 col.separator()
986 col.operator(BTool_Diff.bl_idname, text="Difference", icon="ROTACTIVE")
987 col.operator(BTool_Union.bl_idname, text="Union", icon="ROTATECOLLECTION")
988 col.operator(BTool_Inters.bl_idname, text="Intersect", icon="ROTATECENTER")
989 col.operator(BTool_Slice.bl_idname, text="Slice", icon="ROTATECENTER")
991 main.separator()
993 col = main.column(align=True)
994 col.label("Draw:", icon="MESH_CUBE")
995 col.separator()
996 col.operator(BTool_DrawPolyBrush.bl_idname, icon="LINE_DATA")
999 # ---------- Toolshelf: Properties --------------------------------------------------------
1001 class BoolTool_Config(Panel):
1002 bl_category = "Tools"
1003 bl_label = "Properties"
1004 bl_idname = "BoolTool_BConfig"
1005 bl_space_type = "VIEW_3D"
1006 bl_region_type = "TOOLS"
1007 bl_context = "objectmode"
1009 @classmethod
1010 def poll(cls, context):
1012 result = False
1013 actObj = bpy.context.active_object
1014 if (isCanvas(actObj) or isBrush(actObj) or isPolyBrush(actObj)):
1015 result = True
1016 return result
1018 def draw(self, context):
1019 actObj = bpy.context.active_object
1020 icon = ""
1022 layout = self.layout
1023 row = layout.row(True)
1025 # CANVAS ---------------------------------------------------
1026 if isCanvas(actObj):
1027 row.label("CANVAS", icon="MESH_GRID")
1028 row = layout.row()
1029 row.prop(context.scene, 'BoolHide', text="Hide Bool objects")
1030 row = layout.row(True)
1031 row.operator(BTool_AllBrushToMesh.bl_idname, icon="MOD_LATTICE", text="Apply All")
1033 row = layout.row(True)
1034 Rem = row.operator(BTool_Remove.bl_idname, icon="CANCEL", text="Remove All")
1035 Rem.thisObj = ""
1036 Rem.Prop = "CANVAS"
1038 if isBrush(actObj):
1039 layout.separator()
1041 # BRUSH ------------------------------------------------------
1042 if isBrush(actObj):
1044 if (actObj["BoolToolBrush"] == "UNION"):
1045 icon = "ROTATECOLLECTION"
1046 if (actObj["BoolToolBrush"] == "DIFFERENCE"):
1047 icon = "ROTATECENTER"
1048 if (actObj["BoolToolBrush"] == "INTERSECT"):
1049 icon = "ROTACTIVE"
1050 if (actObj["BoolToolBrush"] == "SLICE"):
1051 icon = "ROTATECENTER"
1053 row = layout.row(True)
1054 row.label("BRUSH", icon=icon)
1056 icon = ""
1057 if actObj["BoolTool_FTransform"] == "True":
1058 icon = "PMARKER_ACT"
1059 else:
1060 icon = "PMARKER"
1061 if isFTransf():
1062 pass
1064 if isFTransf():
1065 row = layout.row(True)
1066 row.operator(BTool_EnableFTransform.bl_idname, text="Fast Vis", icon=icon)
1067 row.operator(BTool_EnableThisBrush.bl_idname, text="Enable", icon="VISIBLE_IPO_ON")
1068 row = layout.row(True)
1069 else:
1070 row.operator(BTool_EnableThisBrush.bl_idname, icon="VISIBLE_IPO_ON")
1071 row = layout.row(True)
1073 if isPolyBrush(actObj):
1074 row = layout.row(False)
1075 row.label("POLY BRUSH", icon="LINE_DATA")
1076 mod = actObj.modifiers["BTool_PolyBrush"]
1077 row = layout.row(False)
1078 row.prop(mod, "thickness", text="Size")
1079 layout.separator()
1081 if isBrush(actObj):
1082 row = layout.row(True)
1083 row.operator(BTool_BrushToMesh.bl_idname, icon="MOD_LATTICE", text="Apply Brush")
1084 row = layout.row(True)
1085 Rem = row.operator(BTool_Remove.bl_idname, icon="CANCEL", text="Remove Brush")
1086 Rem.thisObj = ""
1087 Rem.Prop = "BRUSH"
1089 layout.separator()
1092 # ---------- Toolshelf: Brush Viewer -------------------------------------------------------
1094 class BoolTool_BViwer(Panel):
1095 bl_label = "Brush Viewer"
1096 bl_idname = "BoolTool_BViwer"
1097 bl_space_type = "VIEW_3D"
1098 bl_region_type = "TOOLS"
1099 bl_category = "Tools"
1100 bl_context = "objectmode"
1102 @classmethod
1103 def poll(cls, context):
1104 actObj = bpy.context.active_object
1106 if isCanvas(actObj):
1107 return True
1108 else:
1109 return False
1111 def draw(self, context):
1113 actObj = bpy.context.active_object
1114 icon = ""
1116 if isCanvas(actObj):
1118 for mod in actObj.modifiers:
1119 container = self.layout.box()
1120 row = container.row(True)
1121 icon = ""
1122 if ("BTool_" in mod.name):
1123 if (mod.operation == "UNION"):
1124 icon = "ROTATECOLLECTION"
1125 if (mod.operation == "DIFFERENCE"):
1126 icon = "ROTATECENTER"
1127 if (mod.operation == "INTERSECT"):
1128 icon = "ROTACTIVE"
1129 if (mod.operation == "SLICE"):
1130 icon = "ROTATECENTER"
1132 objSelect = row.operator("btool.find_brush", text=mod.object.name, icon=icon, emboss=False)
1133 objSelect.obj = mod.object.name
1135 EnableIcon = "RESTRICT_VIEW_ON"
1136 if (mod.show_viewport):
1137 EnableIcon = "RESTRICT_VIEW_OFF"
1138 Enable = row.operator(BTool_EnableBrush.bl_idname, icon=EnableIcon, emboss=False)
1139 Enable.thisObj = mod.object.name
1141 Remove = row.operator("btool.remove", icon="CANCEL", emboss=False)
1142 Remove.thisObj = mod.object.name
1143 Remove.Prop = "THIS"
1145 # Stack Changer
1146 Up = row.operator("btool.move_stack", icon="TRIA_UP", emboss=False)
1147 Up.modif = mod.name
1148 Up.direction = "UP"
1150 Dw = row.operator("btool.move_stack", icon="TRIA_DOWN", emboss=False)
1151 Dw.modif = mod.name
1152 Dw.direction = "DOWN"
1154 else:
1155 row.label(mod.name)
1156 # Stack Changer
1157 Up = row.operator("btool.move_stack", icon="TRIA_UP", emboss=False)
1158 Up.modif = mod.name
1159 Up.direction = "UP"
1161 Dw = row.operator("btool.move_stack", icon="TRIA_DOWN", emboss=False)
1162 Dw.modif = mod.name
1163 Dw.direction = "DOWN"
1166 # ------------------ BOOL TOOL Help ----------------------------
1167 class BoolTool_help(Operator):
1168 """Tip"""
1169 bl_idname = "help.bool_tool"
1170 bl_label = ""
1172 def draw(self, context):
1173 layout = self.layout
1174 layout.label("To use:")
1175 layout.label("Select two or more objects.")
1176 layout.label("Auto Boolean:")
1177 layout.label("Apply boolean operation directly.")
1178 layout.label("Brush Boolean:")
1179 layout.label("Create boolean brush set up.")
1181 def execute(self, context):
1182 return {'FINISHED'}
1184 def invoke(self, context, event):
1185 return context.window_manager.invoke_popup(self, width=220)
1188 # ------------------ BOOL TOOL ADD-ON PREFERENCES ----------------------------
1190 def UpdateBoolTool_Pref(self, context):
1191 if self.fast_transform:
1192 RegisterFastT()
1193 else:
1194 UnRegisterFastT()
1197 # Add-ons Preferences Update Panel
1199 # Define Panel classes for updating
1200 panels = [
1201 BoolTool_Tools,
1202 BoolTool_Config,
1203 BoolTool_BViwer,
1207 def update_panel(self, context):
1208 message = "Bool Tool: Updating Panel locations has failed"
1209 try:
1210 for panel in panels:
1211 if "bl_rna" in panel.__dict__:
1212 bpy.utils.unregister_class(panel)
1214 for panel in panels:
1215 panel.bl_category = context.user_preferences.addons[__name__].preferences.category
1216 bpy.utils.register_class(panel)
1218 except Exception as e:
1219 print("\n[{}]\n{}\n\nError:\n{}".format(__name__, message, e))
1220 pass
1223 class BoolTool_Pref(AddonPreferences):
1224 bl_idname = __name__
1226 fast_transform = BoolProperty(
1227 name="Fast Transformations",
1228 default=False,
1229 update=UpdateBoolTool_Pref,
1230 description="Replace the Transform HotKeys (G,R,S)\n"
1231 "for a custom version that can optimize the visualization of Brushes",
1233 make_vertex_groups = BoolProperty(
1234 name="Make Vertex Groups",
1235 default=False,
1236 description="When Applying a Brush to the Object it will create\n"
1237 "a new vertex group for the new faces",
1239 make_boundary = BoolProperty(
1240 name="Make Boundary",
1241 default=False,
1242 description="When Apply a Brush to the Object it will create a\n"
1243 "new vertex group of the bondary boolean area",
1245 use_wire = BoolProperty(
1246 name="Use Bmesh",
1247 default=False,
1248 description="Use The Wireframe Instead of Bounding Box for visualization",
1250 category = StringProperty(
1251 name="Tab Category",
1252 description="Choose a name for the category of the panel",
1253 default="Tools",
1254 update=update_panel,
1256 solver = EnumProperty(
1257 name="Boolean Solver",
1258 items=(('BMESH', "BMesh", "BMesh solver is faster, but less stable "
1259 "and cannot handle coplanar geometry"),
1260 ('CARVE', "Carve", "Carve solver is slower, but more stable "
1261 "and can handle simple cases of coplanar geometry")),
1262 default='BMESH',
1263 description="Specify solver for boolean operations",
1265 Enable_Tab_01 = BoolProperty(
1266 default=False
1269 def draw(self, context):
1270 layout = self.layout
1271 split_percent = 0.3
1273 split = layout.split(percentage=split_percent)
1274 col = split.column()
1275 col.label(text="Tab Category:")
1276 col = split.column()
1277 colrow = col.row()
1278 colrow.prop(self, "category", text="")
1280 split = layout.split(percentage=split_percent)
1281 col = split.column()
1282 col.label("Boolean Solver:")
1283 col = split.column()
1284 colrow = col.row()
1285 colrow.prop(self, "solver", expand=True)
1287 split = layout.split(percentage=split_percent)
1288 col = split.column()
1289 col.label("Experimental Features:")
1290 col = split.column()
1291 colrow = col.row(align=True)
1292 colrow.prop(self, "fast_transform", toggle=True)
1293 colrow.prop(self, "use_wire", text="Use Wire Instead Of Bbox", toggle=True)
1294 layout.separator()
1296 # EXPERIMENTAL
1297 col.prop(self, "make_vertex_groups")
1298 col.prop(self, "make_boundary")
1300 layout.prop(self, "Enable_Tab_01", text="Hot Keys", icon="KEYINGSET")
1301 if self.Enable_Tab_01:
1302 row = layout.row()
1304 col = row.column()
1305 col.label("Hotkey List:")
1306 col.label("Menu: Ctrl Shift B")
1308 row = layout.row()
1309 col = row.column()
1310 col.label("Brush Operators:")
1311 col.label("Union: Ctrl Num +")
1312 col.label("Diff: Ctrl Num -")
1313 col.label("Intersect: Ctrl Num *")
1314 col.label("Slice: Ctrl Num /")
1316 row = layout.row()
1317 col = row.column()
1318 col.label("Auto Operators:")
1319 col.label("Difference: Ctrl Shift Num -")
1320 col.label("Union: Ctrl Shift Num +")
1321 col.label("Intersect: Ctrl Shift Num *")
1322 col.label("Slice: Ctrl Shift Num /")
1323 col.label("BTool Brush To Mesh: Ctrl Num Enter")
1324 col.label("BTool All Brush To Mesh: Ctrl Shift Num Enter")
1327 # ------------------- Class List ------------------------------------------------
1329 classes = (
1330 BoolTool_Pref,
1331 BoolTool_Menu,
1332 BoolTool_Tools,
1333 BoolTool_Config,
1334 BoolTool_BViwer,
1336 Auto_Union,
1337 Auto_Difference,
1338 Auto_Intersect,
1339 Auto_Slice,
1340 Auto_Subtract,
1342 BTool_Union,
1343 BTool_Diff,
1344 BTool_Inters,
1345 BTool_Slice,
1346 BTool_DrawPolyBrush,
1347 BTool_Remove,
1348 BTool_AllBrushToMesh,
1349 BTool_BrushToMesh,
1350 BTool_FindBrush,
1351 BTool_MoveStack,
1352 BTool_EnableBrush,
1353 BTool_EnableThisBrush,
1354 BTool_EnableFTransform,
1355 BTool_FastTransform,
1357 BoolTool_help
1361 # ------------------- REGISTER ------------------------------------------------
1363 addon_keymaps = []
1364 addon_keymapsFastT = []
1367 # Fast Transform HotKeys Register
1368 def RegisterFastT():
1369 wm = bpy.context.window_manager
1370 km = wm.keyconfigs.addon.keymaps.new(name='Object Mode', space_type='EMPTY')
1372 kmi = km.keymap_items.new(BTool_FastTransform.bl_idname, 'G', 'PRESS')
1373 kmi.properties.operator = "Translate"
1374 addon_keymapsFastT.append((km, kmi))
1376 kmi = km.keymap_items.new(BTool_FastTransform.bl_idname, 'R', 'PRESS')
1377 kmi.properties.operator = "Rotate"
1378 addon_keymapsFastT.append((km, kmi))
1380 kmi = km.keymap_items.new(BTool_FastTransform.bl_idname, 'S', 'PRESS')
1381 kmi.properties.operator = "Scale"
1382 addon_keymapsFastT.append((km, kmi))
1385 # Fast Transform HotKeys UnRegister
1386 def UnRegisterFastT():
1387 wm = bpy.context.window_manager
1388 km = wm.keyconfigs.addon.keymaps.new(name='Object Mode', space_type='EMPTY')
1390 for km, kmi in addon_keymapsFastT:
1391 km.keymap_items.remove(kmi)
1392 addon_keymapsFastT.clear()
1395 def register():
1396 for cls in classes:
1397 bpy.utils.register_class(cls)
1398 update_panel(None, bpy.context)
1400 # Scene variables
1401 bpy.types.Scene.BoolHide = BoolProperty(
1402 default=False,
1403 description="Hide boolean objects",
1404 update=update_BoolHide,
1406 # Handlers
1407 bpy.app.handlers.scene_update_post.append(HandleScene)
1409 bpy.types.VIEW3D_MT_object.append(VIEW3D_BoolTool_Menu)
1410 try:
1411 bpy.types.VIEW3D_MT_Object.prepend(VIEW3D_BoolTool_Menu)
1412 except:
1413 pass
1415 wm = bpy.context.window_manager
1417 # create the boolean menu hotkey
1418 km = wm.keyconfigs.addon.keymaps.new(name='Object Mode')
1419 kmi = km.keymap_items.new('wm.call_menu', 'B', 'PRESS', ctrl=True, shift=True)
1420 kmi.properties.name = 'OBJECT_MT_BoolTool_Menu'
1422 # Brush Operators
1423 kmi = km.keymap_items.new(BTool_Union.bl_idname, 'NUMPAD_PLUS', 'PRESS', ctrl=True)
1424 kmi = km.keymap_items.new(BTool_Diff.bl_idname, 'NUMPAD_MINUS', 'PRESS', ctrl=True)
1425 kmi = km.keymap_items.new(BTool_Inters.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', ctrl=True)
1426 kmi = km.keymap_items.new(BTool_Slice.bl_idname, 'NUMPAD_SLASH', 'PRESS', ctrl=True)
1427 kmi = km.keymap_items.new(BTool_BrushToMesh.bl_idname, 'NUMPAD_ENTER', 'PRESS', ctrl=True)
1428 kmi = km.keymap_items.new(BTool_AllBrushToMesh.bl_idname, 'NUMPAD_ENTER', 'PRESS', ctrl=True, shift=True)
1430 # Auto Operators
1431 kmi = km.keymap_items.new(Auto_Union.bl_idname, 'NUMPAD_PLUS', 'PRESS', ctrl=True, shift=True)
1432 kmi = km.keymap_items.new(Auto_Difference.bl_idname, 'NUMPAD_MINUS', 'PRESS', ctrl=True, shift=True)
1433 kmi = km.keymap_items.new(Auto_Intersect.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', ctrl=True, shift=True)
1434 kmi = km.keymap_items.new(Auto_Slice.bl_idname, 'NUMPAD_SLASH', 'PRESS', ctrl=True, shift=True)
1436 addon_keymaps.append(km)
1439 def unregister():
1440 # Keymapping
1441 # remove keymaps when add-on is deactivated
1442 wm = bpy.context.window_manager
1443 for km in addon_keymaps:
1444 wm.keyconfigs.addon.keymaps.remove(km)
1445 del addon_keymaps[:]
1447 bpy.types.VIEW3D_MT_object.remove(VIEW3D_BoolTool_Menu)
1448 try:
1449 bpy.types.VIEW3D_MT_Object.remove(VIEW3D_BoolTool_Menu)
1450 except:
1451 pass
1453 del bpy.types.Scene.BoolHide
1455 for cls in classes:
1456 bpy.utils.unregister_class(cls)
1459 if __name__ == "__main__":
1460 register()