# -*- coding:utf-8 -*- # ##### BEGIN GPL LICENSE BLOCK ##### # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # ##### END GPL LICENSE BLOCK ##### # # ---------------------------------------------------------- # Author: Stephen Leger (s-leger) # # ---------------------------------------------------------- import bpy from bpy.types import Operator from bpy.props import EnumProperty from mathutils import Vector from . archipack_object import ArchipackCollectionManager class ArchipackBoolManager(ArchipackCollectionManager): """ Handle three methods for booleans - interactive: one modifier for each hole right on wall - robust: one single modifier on wall and merge holes in one mesh - mixed: merge holes with boolean and use result on wall may be slow, but is robust """ def __init__(self, mode): """ mode in 'ROBUST', 'INTERACTIVE', 'HYBRID' """ self.mode = mode # internal variables self.itM = None self.min_x = 0 self.min_y = 0 self.min_z = 0 self.max_x = 0 self.max_y = 0 self.max_z = 0 def _get_bounding_box(self, wall): self.itM = wall.matrix_world.inverted() x, y, z = wall.bound_box[0] self.min_x = x self.min_y = y self.min_z = z x, y, z = wall.bound_box[6] self.max_x = x self.max_y = y self.max_z = z self.center = Vector(( self.min_x + 0.5 * (self.max_x - self.min_x), self.min_y + 0.5 * (self.max_y - self.min_y), self.min_z + 0.5 * (self.max_z - self.min_z))) def _contains(self, pt): p = self.itM @ pt return (p.x >= self.min_x and p.x <= self.max_x and p.y >= self.min_y and p.y <= self.max_y and p.z >= self.min_z and p.z <= self.max_z) def filter_wall(self, wall): d = wall.data return (d is None or 'archipack_window' in d or 'archipack_window_panel' in d or 'archipack_door' in d or 'archipack_doorpanel' in d or 'archipack_hole' in wall or 'archipack_robusthole' in wall or 'archipack_handle' in wall) def datablock(self, o): """ get datablock from windows and doors return datablock if found None when not found """ d = None if o.data is None: return if "archipack_window" in o.data: d = o.data.archipack_window[0] elif "archipack_door" in o.data: d = o.data.archipack_door[0] return d def prepare_hole(self, hole): hole.lock_location = (True, True, True) hole.lock_rotation = (True, True, True) hole.lock_scale = (True, True, True) hole.display_type = 'WIRE' hole.hide_render = True hole.hide_select = True hole.select_set(state=True) def get_child_hole(self, o): for hole in o.children: if "archipack_hole" in hole: return hole return None def _generate_hole(self, context, o): # use existing one if self.mode != 'ROBUST': hole = self.get_child_hole(o) if hole is not None: # print("_generate_hole Use existing hole %s" % (hole.name)) return hole # generate single hole from archipack primitives d = self.datablock(o) hole = None if d is not None: if (self.itM is not None and ( self._contains(o.location) or self._contains(o.matrix_world @ Vector((0, 0, 0.5 * d.z)))) ): if self.mode != 'ROBUST': hole = d.interactive_hole(context, o) else: hole = d.robust_hole(context, o.matrix_world) # print("_generate_hole Generate hole %s" % (hole.name)) else: hole = d.interactive_hole(context, o) return hole def partition(self, array, begin, end): pivot = begin for i in range(begin + 1, end + 1): if array[i][1] <= array[begin][1]: pivot += 1 array[i], array[pivot] = array[pivot], array[i] array[pivot], array[begin] = array[begin], array[pivot] return pivot def quicksort(self, array, begin=0, end=None): if end is None: end = len(array) - 1 def _quicksort(array, begin, end): if begin >= end: return pivot = self.partition(array, begin, end) _quicksort(array, begin, pivot - 1) _quicksort(array, pivot + 1, end) return _quicksort(array, begin, end) def sort_holes(self, wall, holes): """ sort hole from center to borders by distance from center may improve nested booleans """ center = wall.matrix_world @ self.center holes = [(o, (o.matrix_world.translation - center).length) for o in holes] self.quicksort(holes) return [o[0] for o in holes] def difference(self, basis, hole, solver=None): # print("difference %s" % (hole.name)) m = basis.modifiers.new('AutoBoolean', 'BOOLEAN') m.operation = 'DIFFERENCE' m.object = hole def union(self, basis, hole): # print("union %s" % (hole.name)) m = basis.modifiers.new('AutoMerge', 'BOOLEAN') m.operation = 'UNION' m.object = hole def remove_modif_and_object(self, context, o, to_delete): # print("remove_modif_and_object removed:%s" % (len(to_delete))) for m, h in to_delete: if m is not None: if m.object is not None: m.object = None o.modifiers.remove(m) if h is not None: self.unlink_object_from_scene(h) bpy.data.objects.remove(h, do_unlink=True) # Mixed def create_merge_basis(self, context, wall): # print("create_merge_basis") h = bpy.data.meshes.new("AutoBoolean") hole_obj = bpy.data.objects.new("AutoBoolean", h) self.link_object_to_scene(context, hole_obj) hole_obj['archipack_hybridhole'] = True if wall.parent is not None: hole_obj.parent = wall.parent hole_obj.matrix_world = wall.matrix_world.copy() for mat in wall.data.materials: hole_obj.data.materials.append(mat) # MaterialUtils.add_wall2_materials(hole_obj) return hole_obj def update_hybrid(self, context, wall, childs, holes): """ Update all holes modifiers remove holes not found in childs robust -> mixed: there is only one object tagged with "archipack_robusthole" interactive -> mixed: many modifisers on wall tagged with "archipack_hole" keep objects """ existing = [] to_delete = [] # robust/interactive -> mixed for m in wall.modifiers: if m.type == 'BOOLEAN': if m.object is None: to_delete.append([m, None]) elif 'archipack_hole' in m.object: h = m.object if h in holes: to_delete.append([m, None]) else: to_delete.append([m, h]) elif 'archipack_robusthole' in m.object: to_delete.append([m, m.object]) # remove modifier and holes not found in new list self.remove_modif_and_object(context, wall, to_delete) m = wall.modifiers.get("AutoMixedBoolean") if m is None: m = wall.modifiers.new('AutoMixedBoolean', 'BOOLEAN') m.operation = 'DIFFERENCE' if m.object is None: hole_obj = self.create_merge_basis(context, wall) else: hole_obj = m.object m.object = hole_obj self.prepare_hole(hole_obj) to_delete = [] # mixed-> mixed for m in hole_obj.modifiers: h = m.object if h in holes: existing.append(h) else: to_delete.append([m, h]) # remove modifier and holes not found in new list self.remove_modif_and_object(context, hole_obj, to_delete) # add modifier and holes not found in existing for h in holes: if h not in existing: self.union(hole_obj, h) # Interactive def update_interactive(self, context, wall, childs, holes): existing = [] to_delete = [] hole_obj = None # mixed-> interactive for m in wall.modifiers: if m.type == 'BOOLEAN': if m.object is not None and 'archipack_hybridhole' in m.object: hole_obj = m.object break if hole_obj is not None: for m in hole_obj.modifiers: h = m.object if h not in holes: to_delete.append([m, h]) # remove modifier and holes not found in new list self.remove_modif_and_object(context, hole_obj, to_delete) self.unlink_object_from_scene(hole_obj) bpy.data.objects.remove(hole_obj, do_unlink=True) to_delete = [] # interactive/robust -> interactive for m in wall.modifiers: if m.type == 'BOOLEAN': if m.object is None: to_delete.append([m, None]) elif 'archipack_hole' in m.object: h = m.object if h in holes: existing.append(h) else: to_delete.append([m, h]) elif 'archipack_robusthole' in m.object: to_delete.append([m, m.object]) # remove modifier and holes not found in new list self.remove_modif_and_object(context, wall, to_delete) # add modifier and holes not found in existing for h in holes: if h not in existing: self.difference(wall, h) # Robust def update_robust(self, context, wall, childs): modif = None to_delete = [] # robust/interactive/mixed -> robust for m in wall.modifiers: if m.type == 'BOOLEAN': if m.object is None: to_delete.append([m, None]) elif 'archipack_robusthole' in m.object: modif = m to_delete.append([None, m.object]) elif 'archipack_hole' in m.object: to_delete.append([m, m.object]) elif 'archipack_hybridhole' in m.object: to_delete.append([m, m.object]) o = m.object for m in o.modifiers: to_delete.append([None, m.object]) # remove modifier and holes self.remove_modif_and_object(context, wall, to_delete) if bool(len(context.selected_objects) > 0): # more than one hole : join, result becomes context.object if len(context.selected_objects) > 1: bpy.ops.object.join() context.object['archipack_robusthole'] = True hole = context.object hole.name = 'AutoBoolean' childs.append(hole) if modif is None: self.difference(wall, hole) else: modif.object = hole elif modif is not None: wall.modifiers.remove(modif) def autoboolean(self, context, wall): """ Entry point for multi-boolean operations like in T panel autoBoolean and RobustBoolean buttons """ if wall.data is not None and "archipack_wall2" in wall.data: # ensure wall modifier is there before any boolean # to support "revival" of applied modifiers m = wall.modifiers.get("Wall") if m is None: wall.select_set(state=True) context.view_layer.objects.active = wall wall.data.archipack_wall2[0].update(context) bpy.ops.object.select_all(action='DESELECT') context.view_layer.objects.active = None childs = [] holes = [] # get wall bounds to find what's inside self._get_bounding_box(wall) # either generate hole or get existing one for o in context.scene.objects: h = self._generate_hole(context, o) if h is not None: holes.append(h) childs.append(o) self.sort_holes(wall, holes) # hole(s) are selected and active after this one for hole in holes: # copy wall material to hole hole.data.materials.clear() for mat in wall.data.materials: hole.data.materials.append(mat) self.prepare_hole(hole) # update / remove / add boolean modifier if self.mode == 'INTERACTIVE': self.update_interactive(context, wall, childs, holes) elif self.mode == 'ROBUST': self.update_robust(context, wall, childs) else: self.update_hybrid(context, wall, childs, holes) bpy.ops.object.select_all(action='DESELECT') # parenting childs to wall reference point if wall.parent is None: x, y, z = wall.bound_box[0] context.scene.cursor.location = wall.matrix_world @ Vector((x, y, z)) # fix issue #9 context.view_layer.objects.active = wall bpy.ops.archipack.reference_point() else: wall.parent.select_set(state=True) context.view_layer.objects.active = wall.parent wall.select_set(state=True) for o in childs: if 'archipack_robusthole' in o: o.hide_select = False o.select_set(state=True) bpy.ops.archipack.parent_to_reference() for o in childs: if 'archipack_robusthole' in o: o.hide_select = True def detect_mode(self, context, wall): for m in wall.modifiers: if m.type == 'BOOLEAN' and m.object is not None: if 'archipack_hole' in m.object: self.mode = 'INTERACTIVE' if 'archipack_hybridhole' in m.object: self.mode = 'HYBRID' if 'archipack_robusthole' in m.object: self.mode = 'ROBUST' def singleboolean(self, context, wall, o): """ Entry point for single boolean operations in use in draw door and windows over wall o is either a window or a door """ # generate holes for crossing window and doors self.itM = wall.matrix_world.inverted() d = self.datablock(o) hole = None hole_obj = None # default mode defined by __init__ self.detect_mode(context, wall) if d is not None: if self.mode != 'ROBUST': hole = d.interactive_hole(context, o) else: hole = d.robust_hole(context, o.matrix_world) if hole is None: return hole.data.materials.clear() for mat in wall.data.materials: hole.data.materials.append(mat) self.prepare_hole(hole) if self.mode == 'INTERACTIVE': # update / remove / add boolean modifier self.difference(wall, hole) elif self.mode == 'HYBRID': m = wall.modifiers.get('AutoMixedBoolean') if m is None: m = wall.modifiers.new('AutoMixedBoolean', 'BOOLEAN') m.operation = 'DIFFERENCE' if m.object is None: hole_obj = self.create_merge_basis(context, wall) m.object = hole_obj else: hole_obj = m.object self.union(hole_obj, hole) bpy.ops.object.select_all(action='DESELECT') # parenting childs to wall reference point if wall.parent is None: x, y, z = wall.bound_box[0] context.scene.cursor.location = wall.matrix_world @ Vector((x, y, z)) # fix issue #9 context.view_layer.objects.active = wall bpy.ops.archipack.reference_point() else: context.view_layer.objects.active = wall.parent if hole_obj is not None: hole_obj.select_set(state=True) wall.select_set(state=True) o.select_set(state=True) bpy.ops.archipack.parent_to_reference() wall.select_set(state=True) context.view_layer.objects.active = wall if "archipack_wall2" in wall.data: d = wall.data.archipack_wall2[0] g = d.get_generator() d.setup_childs(wall, g) d.relocate_childs(context, wall, g) elif "archipack_roof" in wall.data: pass if hole_obj is not None: self.prepare_hole(hole_obj) class ARCHIPACK_OT_single_boolean(Operator): bl_idname = "archipack.single_boolean" bl_label = "SingleBoolean" bl_description = "Add single boolean for doors and windows" bl_category = 'Archipack' bl_options = {'REGISTER', 'UNDO'} mode : EnumProperty( name="Mode", items=( ('INTERACTIVE', 'INTERACTIVE', 'Interactive, fast but may fail', 0), ('ROBUST', 'ROBUST', 'Not interactive, robust', 1), ('HYBRID', 'HYBRID', 'Interactive, slow but robust', 2) ), default='HYBRID' ) """ Wall must be active object window or door must be selected """ @classmethod def poll(cls, context): w = context.active_object return (w is not None and w.data is not None and ("archipack_wall2" in w.data or "archipack_wall" in w.data or "archipack_roof" in w.data) and len(context.selected_objects) == 2 ) def draw(self, context): pass def execute(self, context): if context.mode == "OBJECT": wall = context.active_object manager = ArchipackBoolManager(mode=self.mode) for o in context.selected_objects: if o != wall: manager.singleboolean(context, wall, o) o.select_set(state=False) break wall.select_set(state=True) context.view_layer.objects.active = wall return {'FINISHED'} else: self.report({'WARNING'}, "Archipack: Option only valid in Object mode") return {'CANCELLED'} class ARCHIPACK_OT_auto_boolean(Operator): bl_idname = "archipack.auto_boolean" bl_label = "AutoBoolean" bl_description = "Automatic boolean for doors and windows" bl_category = 'Archipack' bl_options = {'REGISTER', 'UNDO'} mode : EnumProperty( name="Mode", items=( ('INTERACTIVE', 'INTERACTIVE', 'Interactive, fast but may fail', 0), ('ROBUST', 'ROBUST', 'Not interactive, robust', 1), ('HYBRID', 'HYBRID', 'Interactive, slow but robust', 2) ), default='HYBRID' ) def draw(self, context): layout = self.layout row = layout.row() row.prop(self, 'mode') def execute(self, context): if context.mode == "OBJECT": manager = ArchipackBoolManager(mode=self.mode) active = context.view_layer.objects.active walls = [wall for wall in context.selected_objects if not manager.filter_wall(wall)] bpy.ops.object.select_all(action='DESELECT') for wall in walls: manager.autoboolean(context, wall) bpy.ops.object.select_all(action='DESELECT') wall.select_set(state=True) context.view_layer.objects.active = wall if wall.data is not None and 'archipack_wall2' in wall.data: bpy.ops.archipack.wall2_manipulate('EXEC_DEFAULT') # reselect walls bpy.ops.object.select_all(action='DESELECT') for wall in walls: wall.select_set(state=True) context.view_layer.objects.active = active return {'FINISHED'} else: self.report({'WARNING'}, "Archipack: Option only valid in Object mode") return {'CANCELLED'} class ARCHIPACK_OT_generate_hole(Operator): bl_idname = "archipack.generate_hole" bl_label = "Generate hole" bl_description = "Generate interactive hole for doors and windows" bl_category = 'Archipack' bl_options = {'REGISTER', 'UNDO'} def execute(self, context): if context.mode == "OBJECT": manager = ArchipackBoolManager(mode='HYBRID') o = context.active_object d = manager.datablock(o) if d is None: self.report({'WARNING'}, "Archipack: active object must be a door, a window or a roof") return {'CANCELLED'} bpy.ops.object.select_all(action='DESELECT') o.select_set(state=True) context.view_layer.objects.active = o hole = manager._generate_hole(context, o) manager.prepare_hole(hole) hole.select_set(state=False) o.select_set(state=True) context.view_layer.objects.active = o return {'FINISHED'} else: self.report({'WARNING'}, "Archipack: Option only valid in Object mode") return {'CANCELLED'} def register(): bpy.utils.register_class(ARCHIPACK_OT_generate_hole) bpy.utils.register_class(ARCHIPACK_OT_single_boolean) bpy.utils.register_class(ARCHIPACK_OT_auto_boolean) def unregister(): bpy.utils.unregister_class(ARCHIPACK_OT_generate_hole) bpy.utils.unregister_class(ARCHIPACK_OT_single_boolean) bpy.utils.unregister_class(ARCHIPACK_OT_auto_boolean)