blender-addons/archipack/archipack_autoboolean.py

669 lines
23 KiB
Python

# -*- 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 #####
# <pep8 compliant>
# ----------------------------------------------------------
# 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)
hole.cycles_visibility.camera = False
hole.cycles_visibility.diffuse = False
hole.cycles_visibility.glossy = False
hole.cycles_visibility.shadow = False
hole.cycles_visibility.scatter = False
hole.cycles_visibility.transmission = False
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)