Fix mesh_snap_utilities_line running without key-maps available
[blender-addons.git] / archipack / archipack_autoboolean.py
blobf5b6eaf467df4db1a8a9b289a81de54e23069871
1 # -*- coding:utf-8 -*-
3 # ##### BEGIN GPL LICENSE BLOCK #####
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software Foundation,
17 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 # ##### END GPL LICENSE BLOCK #####
21 # <pep8 compliant>
23 # ----------------------------------------------------------
24 # Author: Stephen Leger (s-leger)
26 # ----------------------------------------------------------
27 import bpy
28 from bpy.types import Operator
29 from bpy.props import EnumProperty
30 from mathutils import Vector
31 from . archipack_object import ArchipackCollectionManager
33 class ArchipackBoolManager(ArchipackCollectionManager):
34 """
35 Handle three methods for booleans
36 - interactive: one modifier for each hole right on wall
37 - robust: one single modifier on wall and merge holes in one mesh
38 - mixed: merge holes with boolean and use result on wall
39 may be slow, but is robust
40 """
41 def __init__(self, mode):
42 """
43 mode in 'ROBUST', 'INTERACTIVE', 'HYBRID'
44 """
45 self.mode = mode
46 # internal variables
47 self.itM = None
48 self.min_x = 0
49 self.min_y = 0
50 self.min_z = 0
51 self.max_x = 0
52 self.max_y = 0
53 self.max_z = 0
55 def _get_bounding_box(self, wall):
56 self.itM = wall.matrix_world.inverted()
57 x, y, z = wall.bound_box[0]
58 self.min_x = x
59 self.min_y = y
60 self.min_z = z
61 x, y, z = wall.bound_box[6]
62 self.max_x = x
63 self.max_y = y
64 self.max_z = z
65 self.center = Vector((
66 self.min_x + 0.5 * (self.max_x - self.min_x),
67 self.min_y + 0.5 * (self.max_y - self.min_y),
68 self.min_z + 0.5 * (self.max_z - self.min_z)))
70 def _contains(self, pt):
71 p = self.itM @ pt
72 return (p.x >= self.min_x and p.x <= self.max_x and
73 p.y >= self.min_y and p.y <= self.max_y and
74 p.z >= self.min_z and p.z <= self.max_z)
76 def filter_wall(self, wall):
77 d = wall.data
78 return (d is None or
79 'archipack_window' in d or
80 'archipack_window_panel' in d or
81 'archipack_door' in d or
82 'archipack_doorpanel' in d or
83 'archipack_hole' in wall or
84 'archipack_robusthole' in wall or
85 'archipack_handle' in wall)
87 def datablock(self, o):
88 """
89 get datablock from windows and doors
90 return
91 datablock if found
92 None when not found
93 """
94 d = None
95 if o.data is None:
96 return
97 if "archipack_window" in o.data:
98 d = o.data.archipack_window[0]
99 elif "archipack_door" in o.data:
100 d = o.data.archipack_door[0]
101 return d
103 def prepare_hole(self, hole):
104 hole.lock_location = (True, True, True)
105 hole.lock_rotation = (True, True, True)
106 hole.lock_scale = (True, True, True)
107 hole.display_type = 'WIRE'
108 hole.hide_render = True
109 hole.hide_select = True
110 hole.select_set(state=True)
111 hole.cycles_visibility.camera = False
112 hole.cycles_visibility.diffuse = False
113 hole.cycles_visibility.glossy = False
114 hole.cycles_visibility.shadow = False
115 hole.cycles_visibility.scatter = False
116 hole.cycles_visibility.transmission = False
118 def get_child_hole(self, o):
119 for hole in o.children:
120 if "archipack_hole" in hole:
121 return hole
122 return None
124 def _generate_hole(self, context, o):
125 # use existing one
126 if self.mode != 'ROBUST':
127 hole = self.get_child_hole(o)
128 if hole is not None:
129 # print("_generate_hole Use existing hole %s" % (hole.name))
130 return hole
131 # generate single hole from archipack primitives
132 d = self.datablock(o)
133 hole = None
134 if d is not None:
135 if (self.itM is not None and (
136 self._contains(o.location) or
137 self._contains(o.matrix_world @ Vector((0, 0, 0.5 * d.z))))
139 if self.mode != 'ROBUST':
140 hole = d.interactive_hole(context, o)
141 else:
142 hole = d.robust_hole(context, o.matrix_world)
143 # print("_generate_hole Generate hole %s" % (hole.name))
144 else:
145 hole = d.interactive_hole(context, o)
146 return hole
148 def partition(self, array, begin, end):
149 pivot = begin
150 for i in range(begin + 1, end + 1):
151 if array[i][1] <= array[begin][1]:
152 pivot += 1
153 array[i], array[pivot] = array[pivot], array[i]
154 array[pivot], array[begin] = array[begin], array[pivot]
155 return pivot
157 def quicksort(self, array, begin=0, end=None):
158 if end is None:
159 end = len(array) - 1
161 def _quicksort(array, begin, end):
162 if begin >= end:
163 return
164 pivot = self.partition(array, begin, end)
165 _quicksort(array, begin, pivot - 1)
166 _quicksort(array, pivot + 1, end)
167 return _quicksort(array, begin, end)
169 def sort_holes(self, wall, holes):
171 sort hole from center to borders by distance from center
172 may improve nested booleans
174 center = wall.matrix_world @ self.center
175 holes = [(o, (o.matrix_world.translation - center).length) for o in holes]
176 self.quicksort(holes)
177 return [o[0] for o in holes]
179 def difference(self, basis, hole, solver=None):
180 # print("difference %s" % (hole.name))
181 m = basis.modifiers.new('AutoBoolean', 'BOOLEAN')
182 m.operation = 'DIFFERENCE'
183 m.object = hole
185 def union(self, basis, hole):
186 # print("union %s" % (hole.name))
187 m = basis.modifiers.new('AutoMerge', 'BOOLEAN')
188 m.operation = 'UNION'
189 m.object = hole
191 def remove_modif_and_object(self, context, o, to_delete):
192 # print("remove_modif_and_object removed:%s" % (len(to_delete)))
193 for m, h in to_delete:
194 if m is not None:
195 if m.object is not None:
196 m.object = None
197 o.modifiers.remove(m)
198 if h is not None:
199 self.unlink_object_from_scene(h)
200 bpy.data.objects.remove(h, do_unlink=True)
202 # Mixed
203 def create_merge_basis(self, context, wall):
204 # print("create_merge_basis")
205 h = bpy.data.meshes.new("AutoBoolean")
206 hole_obj = bpy.data.objects.new("AutoBoolean", h)
207 self.link_object_to_scene(context, hole_obj)
208 hole_obj['archipack_hybridhole'] = True
209 if wall.parent is not None:
210 hole_obj.parent = wall.parent
211 hole_obj.matrix_world = wall.matrix_world.copy()
212 for mat in wall.data.materials:
213 hole_obj.data.materials.append(mat)
214 # MaterialUtils.add_wall2_materials(hole_obj)
215 return hole_obj
217 def update_hybrid(self, context, wall, childs, holes):
219 Update all holes modifiers
220 remove holes not found in childs
222 robust -> mixed:
223 there is only one object tagged with "archipack_robusthole"
224 interactive -> mixed:
225 many modifisers on wall tagged with "archipack_hole"
226 keep objects
228 existing = []
229 to_delete = []
231 # robust/interactive -> mixed
232 for m in wall.modifiers:
233 if m.type == 'BOOLEAN':
234 if m.object is None:
235 to_delete.append([m, None])
236 elif 'archipack_hole' in m.object:
237 h = m.object
238 if h in holes:
239 to_delete.append([m, None])
240 else:
241 to_delete.append([m, h])
242 elif 'archipack_robusthole' in m.object:
243 to_delete.append([m, m.object])
245 # remove modifier and holes not found in new list
246 self.remove_modif_and_object(context, wall, to_delete)
248 m = wall.modifiers.get("AutoMixedBoolean")
249 if m is None:
250 m = wall.modifiers.new('AutoMixedBoolean', 'BOOLEAN')
251 m.operation = 'DIFFERENCE'
253 if m.object is None:
254 hole_obj = self.create_merge_basis(context, wall)
255 else:
256 hole_obj = m.object
258 m.object = hole_obj
259 self.prepare_hole(hole_obj)
261 to_delete = []
263 # mixed-> mixed
264 for m in hole_obj.modifiers:
265 h = m.object
266 if h in holes:
267 existing.append(h)
268 else:
269 to_delete.append([m, h])
271 # remove modifier and holes not found in new list
272 self.remove_modif_and_object(context, hole_obj, to_delete)
274 # add modifier and holes not found in existing
275 for h in holes:
276 if h not in existing:
277 self.union(hole_obj, h)
279 # Interactive
280 def update_interactive(self, context, wall, childs, holes):
282 existing = []
284 to_delete = []
286 hole_obj = None
288 # mixed-> interactive
289 for m in wall.modifiers:
290 if m.type == 'BOOLEAN':
291 if m.object is not None and 'archipack_hybridhole' in m.object:
292 hole_obj = m.object
293 break
295 if hole_obj is not None:
296 for m in hole_obj.modifiers:
297 h = m.object
298 if h not in holes:
299 to_delete.append([m, h])
300 # remove modifier and holes not found in new list
301 self.remove_modif_and_object(context, hole_obj, to_delete)
302 self.unlink_object_from_scene(hole_obj)
303 bpy.data.objects.remove(hole_obj, do_unlink=True)
305 to_delete = []
307 # interactive/robust -> interactive
308 for m in wall.modifiers:
309 if m.type == 'BOOLEAN':
310 if m.object is None:
311 to_delete.append([m, None])
312 elif 'archipack_hole' in m.object:
313 h = m.object
314 if h in holes:
315 existing.append(h)
316 else:
317 to_delete.append([m, h])
318 elif 'archipack_robusthole' in m.object:
319 to_delete.append([m, m.object])
321 # remove modifier and holes not found in new list
322 self.remove_modif_and_object(context, wall, to_delete)
324 # add modifier and holes not found in existing
325 for h in holes:
326 if h not in existing:
327 self.difference(wall, h)
329 # Robust
330 def update_robust(self, context, wall, childs):
332 modif = None
334 to_delete = []
336 # robust/interactive/mixed -> robust
337 for m in wall.modifiers:
338 if m.type == 'BOOLEAN':
339 if m.object is None:
340 to_delete.append([m, None])
341 elif 'archipack_robusthole' in m.object:
342 modif = m
343 to_delete.append([None, m.object])
344 elif 'archipack_hole' in m.object:
345 to_delete.append([m, m.object])
346 elif 'archipack_hybridhole' in m.object:
347 to_delete.append([m, m.object])
348 o = m.object
349 for m in o.modifiers:
350 to_delete.append([None, m.object])
352 # remove modifier and holes
353 self.remove_modif_and_object(context, wall, to_delete)
355 if bool(len(context.selected_objects) > 0):
356 # more than one hole : join, result becomes context.object
357 if len(context.selected_objects) > 1:
358 bpy.ops.object.join()
359 context.object['archipack_robusthole'] = True
361 hole = context.object
362 hole.name = 'AutoBoolean'
364 childs.append(hole)
366 if modif is None:
367 self.difference(wall, hole)
368 else:
369 modif.object = hole
370 elif modif is not None:
371 wall.modifiers.remove(modif)
373 def autoboolean(self, context, wall):
375 Entry point for multi-boolean operations like
376 in T panel autoBoolean and RobustBoolean buttons
379 if wall.data is not None and "archipack_wall2" in wall.data:
380 # ensure wall modifier is there before any boolean
381 # to support "revival" of applied modifiers
382 m = wall.modifiers.get("Wall")
383 if m is None:
384 wall.select_set(state=True)
385 context.view_layer.objects.active = wall
386 wall.data.archipack_wall2[0].update(context)
388 bpy.ops.object.select_all(action='DESELECT')
389 context.view_layer.objects.active = None
390 childs = []
391 holes = []
392 # get wall bounds to find what's inside
393 self._get_bounding_box(wall)
395 # either generate hole or get existing one
396 for o in context.scene.objects:
397 h = self._generate_hole(context, o)
398 if h is not None:
399 holes.append(h)
400 childs.append(o)
402 self.sort_holes(wall, holes)
404 # hole(s) are selected and active after this one
405 for hole in holes:
406 # copy wall material to hole
407 hole.data.materials.clear()
408 for mat in wall.data.materials:
409 hole.data.materials.append(mat)
411 self.prepare_hole(hole)
413 # update / remove / add boolean modifier
414 if self.mode == 'INTERACTIVE':
415 self.update_interactive(context, wall, childs, holes)
416 elif self.mode == 'ROBUST':
417 self.update_robust(context, wall, childs)
418 else:
419 self.update_hybrid(context, wall, childs, holes)
421 bpy.ops.object.select_all(action='DESELECT')
422 # parenting childs to wall reference point
423 if wall.parent is None:
424 x, y, z = wall.bound_box[0]
425 context.scene.cursor.location = wall.matrix_world @ Vector((x, y, z))
426 # fix issue #9
427 context.view_layer.objects.active = wall
428 bpy.ops.archipack.reference_point()
429 else:
430 wall.parent.select_set(state=True)
431 context.view_layer.objects.active = wall.parent
433 wall.select_set(state=True)
434 for o in childs:
435 if 'archipack_robusthole' in o:
436 o.hide_select = False
437 o.select_set(state=True)
439 bpy.ops.archipack.parent_to_reference()
441 for o in childs:
442 if 'archipack_robusthole' in o:
443 o.hide_select = True
445 def detect_mode(self, context, wall):
446 for m in wall.modifiers:
447 if m.type == 'BOOLEAN' and m.object is not None:
448 if 'archipack_hole' in m.object:
449 self.mode = 'INTERACTIVE'
450 if 'archipack_hybridhole' in m.object:
451 self.mode = 'HYBRID'
452 if 'archipack_robusthole' in m.object:
453 self.mode = 'ROBUST'
455 def singleboolean(self, context, wall, o):
457 Entry point for single boolean operations
458 in use in draw door and windows over wall
459 o is either a window or a door
462 # generate holes for crossing window and doors
463 self.itM = wall.matrix_world.inverted()
464 d = self.datablock(o)
466 hole = None
467 hole_obj = None
468 # default mode defined by __init__
469 self.detect_mode(context, wall)
471 if d is not None:
472 if self.mode != 'ROBUST':
473 hole = d.interactive_hole(context, o)
474 else:
475 hole = d.robust_hole(context, o.matrix_world)
476 if hole is None:
477 return
479 hole.data.materials.clear()
480 for mat in wall.data.materials:
481 hole.data.materials.append(mat)
483 self.prepare_hole(hole)
485 if self.mode == 'INTERACTIVE':
486 # update / remove / add boolean modifier
487 self.difference(wall, hole)
489 elif self.mode == 'HYBRID':
490 m = wall.modifiers.get('AutoMixedBoolean')
492 if m is None:
493 m = wall.modifiers.new('AutoMixedBoolean', 'BOOLEAN')
494 m.operation = 'DIFFERENCE'
496 if m.object is None:
497 hole_obj = self.create_merge_basis(context, wall)
498 m.object = hole_obj
499 else:
500 hole_obj = m.object
501 self.union(hole_obj, hole)
503 bpy.ops.object.select_all(action='DESELECT')
505 # parenting childs to wall reference point
506 if wall.parent is None:
507 x, y, z = wall.bound_box[0]
508 context.scene.cursor.location = wall.matrix_world @ Vector((x, y, z))
509 # fix issue #9
510 context.view_layer.objects.active = wall
511 bpy.ops.archipack.reference_point()
512 else:
513 context.view_layer.objects.active = wall.parent
515 if hole_obj is not None:
516 hole_obj.select_set(state=True)
518 wall.select_set(state=True)
519 o.select_set(state=True)
520 bpy.ops.archipack.parent_to_reference()
521 wall.select_set(state=True)
522 context.view_layer.objects.active = wall
523 if "archipack_wall2" in wall.data:
524 d = wall.data.archipack_wall2[0]
525 g = d.get_generator()
526 d.setup_childs(wall, g)
527 d.relocate_childs(context, wall, g)
528 elif "archipack_roof" in wall.data:
529 pass
530 if hole_obj is not None:
531 self.prepare_hole(hole_obj)
534 class ARCHIPACK_OT_single_boolean(Operator):
535 bl_idname = "archipack.single_boolean"
536 bl_label = "SingleBoolean"
537 bl_description = "Add single boolean for doors and windows"
538 bl_category = 'Archipack'
539 bl_options = {'REGISTER', 'UNDO'}
540 mode : EnumProperty(
541 name="Mode",
542 items=(
543 ('INTERACTIVE', 'INTERACTIVE', 'Interactive, fast but may fail', 0),
544 ('ROBUST', 'ROBUST', 'Not interactive, robust', 1),
545 ('HYBRID', 'HYBRID', 'Interactive, slow but robust', 2)
547 default='HYBRID'
550 Wall must be active object
551 window or door must be selected
554 @classmethod
555 def poll(cls, context):
556 w = context.active_object
557 return (w is not None and w.data is not None and
558 ("archipack_wall2" in w.data or
559 "archipack_wall" in w.data or
560 "archipack_roof" in w.data) and
561 len(context.selected_objects) == 2
564 def draw(self, context):
565 pass
567 def execute(self, context):
568 if context.mode == "OBJECT":
569 wall = context.active_object
570 manager = ArchipackBoolManager(mode=self.mode)
571 for o in context.selected_objects:
572 if o != wall:
573 manager.singleboolean(context, wall, o)
574 o.select_set(state=False)
575 break
576 wall.select_set(state=True)
577 context.view_layer.objects.active = wall
578 return {'FINISHED'}
579 else:
580 self.report({'WARNING'}, "Archipack: Option only valid in Object mode")
581 return {'CANCELLED'}
584 class ARCHIPACK_OT_auto_boolean(Operator):
585 bl_idname = "archipack.auto_boolean"
586 bl_label = "AutoBoolean"
587 bl_description = "Automatic boolean for doors and windows"
588 bl_category = 'Archipack'
589 bl_options = {'REGISTER', 'UNDO'}
590 mode : EnumProperty(
591 name="Mode",
592 items=(
593 ('INTERACTIVE', 'INTERACTIVE', 'Interactive, fast but may fail', 0),
594 ('ROBUST', 'ROBUST', 'Not interactive, robust', 1),
595 ('HYBRID', 'HYBRID', 'Interactive, slow but robust', 2)
597 default='HYBRID'
600 def draw(self, context):
601 layout = self.layout
602 row = layout.row()
603 row.prop(self, 'mode')
605 def execute(self, context):
606 if context.mode == "OBJECT":
607 manager = ArchipackBoolManager(mode=self.mode)
608 active = context.view_layer.objects.active
609 walls = [wall for wall in context.selected_objects if not manager.filter_wall(wall)]
610 bpy.ops.object.select_all(action='DESELECT')
611 for wall in walls:
612 manager.autoboolean(context, wall)
613 bpy.ops.object.select_all(action='DESELECT')
614 wall.select_set(state=True)
615 context.view_layer.objects.active = wall
616 if wall.data is not None and 'archipack_wall2' in wall.data:
617 bpy.ops.archipack.wall2_manipulate('EXEC_DEFAULT')
618 # reselect walls
619 bpy.ops.object.select_all(action='DESELECT')
620 for wall in walls:
621 wall.select_set(state=True)
622 context.view_layer.objects.active = active
623 return {'FINISHED'}
624 else:
625 self.report({'WARNING'}, "Archipack: Option only valid in Object mode")
626 return {'CANCELLED'}
629 class ARCHIPACK_OT_generate_hole(Operator):
630 bl_idname = "archipack.generate_hole"
631 bl_label = "Generate hole"
632 bl_description = "Generate interactive hole for doors and windows"
633 bl_category = 'Archipack'
634 bl_options = {'REGISTER', 'UNDO'}
636 def execute(self, context):
637 if context.mode == "OBJECT":
638 manager = ArchipackBoolManager(mode='HYBRID')
639 o = context.active_object
641 d = manager.datablock(o)
642 if d is None:
643 self.report({'WARNING'}, "Archipack: active object must be a door, a window or a roof")
644 return {'CANCELLED'}
645 bpy.ops.object.select_all(action='DESELECT')
646 o.select_set(state=True)
647 context.view_layer.objects.active = o
648 hole = manager._generate_hole(context, o)
649 manager.prepare_hole(hole)
650 hole.select_set(state=False)
651 o.select_set(state=True)
652 context.view_layer.objects.active = o
653 return {'FINISHED'}
654 else:
655 self.report({'WARNING'}, "Archipack: Option only valid in Object mode")
656 return {'CANCELLED'}
659 def register():
660 bpy.utils.register_class(ARCHIPACK_OT_generate_hole)
661 bpy.utils.register_class(ARCHIPACK_OT_single_boolean)
662 bpy.utils.register_class(ARCHIPACK_OT_auto_boolean)
665 def unregister():
666 bpy.utils.unregister_class(ARCHIPACK_OT_generate_hole)
667 bpy.utils.unregister_class(ARCHIPACK_OT_single_boolean)
668 bpy.utils.unregister_class(ARCHIPACK_OT_auto_boolean)