Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStephen Leger <stephen@3dservices.ch>2017-07-22 14:25:28 +0300
committerStephen Leger <stephen@3dservices.ch>2017-07-22 14:26:04 +0300
commitc1ab9b4b9c6c0226f8d7789b92efda9b0f33cfd1 (patch)
tree37d5a97c758fa9af48d1dfb5428edd72072d882a /archipack/archipack_autoboolean.py
parent5638a8783502138500912061dde0e8ee476d7fca (diff)
archipack: T52120 release to official
Diffstat (limited to 'archipack/archipack_autoboolean.py')
-rw-r--r--archipack/archipack_autoboolean.py678
1 files changed, 678 insertions, 0 deletions
diff --git a/archipack/archipack_autoboolean.py b/archipack/archipack_autoboolean.py
new file mode 100644
index 00000000..a171532c
--- /dev/null
+++ b/archipack/archipack_autoboolean.py
@@ -0,0 +1,678 @@
+# -*- 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 .materialutils import MaterialUtils
+
+from os import path
+
+
+def debug_using_gl(context, filename):
+ context.scene.update()
+ temp_path = "C:\\tmp\\"
+ context.scene.render.filepath = path.join(temp_path, filename + ".png")
+ bpy.ops.render.opengl(write_still=True)
+
+
+class ArchipackBoolManager():
+ """
+ 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, solver_mode='CARVE'):
+ """
+ mode in 'ROBUST', 'INTERACTIVE', 'HYBRID'
+ """
+ self.mode = mode
+ self.solver_mode = solver_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:
+ 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.draw_type = 'WIRE'
+ hole.hide_render = True
+ hole.hide_select = True
+ hole.select = 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'
+ if solver is None:
+ m.solver = self.solver_mode
+ else:
+ m.solver = solver
+ m.object = hole
+
+ def union(self, basis, hole):
+ # print("union %s" % (hole.name))
+ m = basis.modifiers.new('AutoMerge', 'BOOLEAN')
+ m.operation = 'UNION'
+ m.solver = self.solver_mode
+ 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:
+ context.scene.objects.unlink(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)
+ context.scene.objects.link(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()
+ 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 taged with "archipack_robusthole"
+ interactive -> mixed:
+ many modifisers on wall taged 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.solver = self.solver_mode
+ m.operation = 'DIFFERENCE'
+
+ if m.object is None:
+ hole_obj = self.create_merge_basis(context, wall)
+ else:
+ hole_obj = m.object
+ # debug_using_gl(context, "260")
+ m.object = hole_obj
+ self.prepare_hole(hole_obj)
+ # debug_using_gl(context, "263")
+ 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)
+ # debug_using_gl(context, "276")
+ # add modifier and holes not found in existing
+ for h in holes:
+ if h not in existing:
+ self.union(hole_obj, h)
+ # debug_using_gl(context, "281")
+
+ # 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)
+ context.scene.objects.unlink(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
+ """
+ bpy.ops.object.select_all(action='DESELECT')
+ context.scene.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)
+ # debug_using_gl(context, "395")
+ self.sort_holes(wall, holes)
+
+ # hole(s) are selected and active after this one
+ for hole in holes:
+ self.prepare_hole(hole)
+ # debug_using_gl(context, "401")
+
+ # 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.scene.objects.active = wall
+ bpy.ops.archipack.reference_point()
+ else:
+ wall.parent.select = True
+ context.scene.objects.active = wall.parent
+ # debug_using_gl(context, "422")
+ wall.select = True
+ for o in childs:
+ if 'archipack_robusthole' in o:
+ o.hide_select = False
+ o.select = True
+ # debug_using_gl(context, "428")
+
+ bpy.ops.archipack.parent_to_reference()
+
+ for o in childs:
+ if 'archipack_robusthole' in o:
+ o.hide_select = True
+ # debug_using_gl(context, "435")
+
+ 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
+
+ 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'
+ m.solver = self.solver_mode
+
+ 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.scene.objects.active = wall
+ bpy.ops.archipack.reference_point()
+ else:
+ context.scene.objects.active = wall.parent
+
+ if hole_obj is not None:
+ hole_obj.select = True
+
+ wall.select = True
+ o.select = True
+ bpy.ops.archipack.parent_to_reference()
+ wall.select = True
+ context.scene.objects.active = wall
+ d = wall.data.archipack_wall2[0]
+ g = d.get_generator()
+ d.setup_childs(wall, g)
+ d.relocate_childs(context, wall, g)
+
+ 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'
+ )
+ solver_mode = EnumProperty(
+ name="Solver",
+ items=(
+ ('CARVE', 'CARVE', 'Slow but robust (could be slow in hybrid mode with many holes)', 0),
+ ('BMESH', 'BMESH', 'Fast but more prone to errors', 1)
+ ),
+ default='BMESH'
+ )
+ """
+ Wall must be active object
+ window or door must be selected
+ """
+
+ @classmethod
+ def poll(cls, context):
+ w = context.active_object
+ return (w.data is not None and
+ "archipack_wall2" 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, solver_mode=self.solver_mode)
+ for o in context.selected_objects:
+ if o != wall:
+ manager.singleboolean(context, wall, o)
+ break
+ o.select = False
+ wall.select = True
+ context.scene.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'
+ )
+ solver_mode = EnumProperty(
+ name="Solver",
+ items=(
+ ('CARVE', 'CARVE', 'Slow but robust (could be slow in hybrid mode with many holes)', 0),
+ ('BMESH', 'BMESH', 'Fast but more prone to errors', 1)
+ ),
+ default='BMESH'
+ )
+
+ def draw(self, context):
+ layout = self.layout
+ row = layout.row()
+ row.prop(self, 'mode')
+ row.prop(self, 'solver_mode')
+
+ def execute(self, context):
+ if context.mode == "OBJECT":
+ manager = ArchipackBoolManager(mode=self.mode, solver_mode=self.solver_mode)
+ active = context.scene.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 = True
+ context.scene.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 = True
+ context.scene.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 or a window")
+ return {'CANCELLED'}
+ bpy.ops.object.select_all(action='DESELECT')
+ o.select = True
+ context.scene.objects.active = o
+ hole = manager._generate_hole(context, o)
+ manager.prepare_hole(hole)
+ hole.select = False
+ o.select = True
+ context.scene.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)