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 #####
23 # ----------------------------------------------------------
24 # Author: Stephen Leger (s-leger)
26 # ----------------------------------------------------------
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
):
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
41 def __init__(self
, mode
):
43 mode in 'ROBUST', 'INTERACTIVE', 'HYBRID'
55 def _get_bounding_box(self
, wall
):
56 self
.itM
= wall
.matrix_world
.inverted()
57 x
, y
, z
= wall
.bound_box
[0]
61 x
, y
, z
= wall
.bound_box
[6]
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
):
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
):
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
):
89 get datablock from windows and doors
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]
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
:
124 def _generate_hole(self
, context
, o
):
126 if self
.mode
!= 'ROBUST':
127 hole
= self
.get_child_hole(o
)
129 # print("_generate_hole Use existing hole %s" % (hole.name))
131 # generate single hole from archipack primitives
132 d
= self
.datablock(o
)
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
)
142 hole
= d
.robust_hole(context
, o
.matrix_world
)
143 # print("_generate_hole Generate hole %s" % (hole.name))
145 hole
= d
.interactive_hole(context
, o
)
148 def partition(self
, array
, begin
, end
):
150 for i
in range(begin
+ 1, end
+ 1):
151 if array
[i
][1] <= array
[begin
][1]:
153 array
[i
], array
[pivot
] = array
[pivot
], array
[i
]
154 array
[pivot
], array
[begin
] = array
[begin
], array
[pivot
]
157 def quicksort(self
, array
, begin
=0, end
=None):
161 def _quicksort(array
, begin
, end
):
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'
185 def union(self
, basis
, hole
):
186 # print("union %s" % (hole.name))
187 m
= basis
.modifiers
.new('AutoMerge', 'BOOLEAN')
188 m
.operation
= 'UNION'
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
:
195 if m
.object is not None:
197 o
.modifiers
.remove(m
)
199 self
.unlink_object_from_scene(h
)
200 bpy
.data
.objects
.remove(h
, do_unlink
=True)
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)
217 def update_hybrid(self
, context
, wall
, childs
, holes
):
219 Update all holes modifiers
220 remove holes not found in childs
223 there is only one object tagged with "archipack_robusthole"
224 interactive -> mixed:
225 many modifisers on wall tagged with "archipack_hole"
231 # robust/interactive -> mixed
232 for m
in wall
.modifiers
:
233 if m
.type == 'BOOLEAN':
235 to_delete
.append([m
, None])
236 elif 'archipack_hole' in m
.object:
239 to_delete
.append([m
, None])
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")
250 m
= wall
.modifiers
.new('AutoMixedBoolean', 'BOOLEAN')
251 m
.operation
= 'DIFFERENCE'
254 hole_obj
= self
.create_merge_basis(context
, wall
)
259 self
.prepare_hole(hole_obj
)
264 for m
in hole_obj
.modifiers
:
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
276 if h
not in existing
:
277 self
.union(hole_obj
, h
)
280 def update_interactive(self
, context
, wall
, childs
, holes
):
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:
295 if hole_obj
is not None:
296 for m
in hole_obj
.modifiers
:
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)
307 # interactive/robust -> interactive
308 for m
in wall
.modifiers
:
309 if m
.type == 'BOOLEAN':
311 to_delete
.append([m
, None])
312 elif 'archipack_hole' in m
.object:
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
326 if h
not in existing
:
327 self
.difference(wall
, h
)
330 def update_robust(self
, context
, wall
, childs
):
336 # robust/interactive/mixed -> robust
337 for m
in wall
.modifiers
:
338 if m
.type == 'BOOLEAN':
340 to_delete
.append([m
, None])
341 elif 'archipack_robusthole' in m
.object:
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])
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'
367 self
.difference(wall
, 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")
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
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
)
402 self
.sort_holes(wall
, holes
)
404 # hole(s) are selected and active after this one
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
)
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
))
427 context
.view_layer
.objects
.active
= wall
428 bpy
.ops
.archipack
.reference_point()
430 wall
.parent
.select_set(state
=True)
431 context
.view_layer
.objects
.active
= wall
.parent
433 wall
.select_set(state
=True)
435 if 'archipack_robusthole' in o
:
436 o
.hide_select
= False
437 o
.select_set(state
=True)
439 bpy
.ops
.archipack
.parent_to_reference()
442 if 'archipack_robusthole' in o
:
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:
452 if 'archipack_robusthole' in m
.object:
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
)
468 # default mode defined by __init__
469 self
.detect_mode(context
, wall
)
472 if self
.mode
!= 'ROBUST':
473 hole
= d
.interactive_hole(context
, o
)
475 hole
= d
.robust_hole(context
, o
.matrix_world
)
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')
493 m
= wall
.modifiers
.new('AutoMixedBoolean', 'BOOLEAN')
494 m
.operation
= 'DIFFERENCE'
497 hole_obj
= self
.create_merge_basis(context
, wall
)
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
))
510 context
.view_layer
.objects
.active
= wall
511 bpy
.ops
.archipack
.reference_point()
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
:
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'}
543 ('INTERACTIVE', 'INTERACTIVE', 'Interactive, fast but may fail', 0),
544 ('ROBUST', 'ROBUST', 'Not interactive, robust', 1),
545 ('HYBRID', 'HYBRID', 'Interactive, slow but robust', 2)
550 Wall must be active object
551 window or door must be selected
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
):
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
:
573 manager
.singleboolean(context
, wall
, o
)
574 o
.select_set(state
=False)
576 wall
.select_set(state
=True)
577 context
.view_layer
.objects
.active
= wall
580 self
.report({'WARNING'}, "Archipack: Option only valid in Object mode")
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'}
593 ('INTERACTIVE', 'INTERACTIVE', 'Interactive, fast but may fail', 0),
594 ('ROBUST', 'ROBUST', 'Not interactive, robust', 1),
595 ('HYBRID', 'HYBRID', 'Interactive, slow but robust', 2)
600 def draw(self
, context
):
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')
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')
619 bpy
.ops
.object.select_all(action
='DESELECT')
621 wall
.select_set(state
=True)
622 context
.view_layer
.objects
.active
= active
625 self
.report({'WARNING'}, "Archipack: Option only valid in Object mode")
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
)
643 self
.report({'WARNING'}, "Archipack: active object must be a door, a window or a roof")
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
655 self
.report({'WARNING'}, "Archipack: Option only valid in Object mode")
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
)
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
)