update addons (for 5.2)
This commit is contained in:
@@ -18,12 +18,12 @@ else:
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
modules = [
|
||||
modules = (
|
||||
boolean,
|
||||
canvas,
|
||||
cutter,
|
||||
select,
|
||||
]
|
||||
)
|
||||
|
||||
def register():
|
||||
for module in modules:
|
||||
|
||||
@@ -2,12 +2,13 @@ import bpy
|
||||
from collections import defaultdict
|
||||
from .. import __package__ as base_package
|
||||
|
||||
from ..functions.poll import (
|
||||
basic_poll,
|
||||
is_linked,
|
||||
is_instanced_data,
|
||||
list_candidate_objects,
|
||||
destructive_op_confirmation,
|
||||
from ..functions.canvas import (
|
||||
filter_canvases,
|
||||
create_slice,
|
||||
)
|
||||
from ..functions.cutter import (
|
||||
filter_cutters,
|
||||
make_cutter,
|
||||
)
|
||||
from ..functions.modifier import (
|
||||
add_boolean_modifier,
|
||||
@@ -15,23 +16,36 @@ from ..functions.modifier import (
|
||||
get_modifiers_to_apply,
|
||||
)
|
||||
from ..functions.object import (
|
||||
set_cutter_properties,
|
||||
change_parent,
|
||||
create_slice,
|
||||
delete_cutter,
|
||||
delete_object,
|
||||
)
|
||||
from ..functions.poll import (
|
||||
basic_poll,
|
||||
convert_to_mesh_confirmation,
|
||||
destructive_op_confirmation,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ PROPERTIES ------------------------------ ####
|
||||
|
||||
class ModifierProperties():
|
||||
class BooleanBase():
|
||||
# Add-on properties.
|
||||
flip: bpy.props.BoolProperty(
|
||||
name = "Flip Canvas & Cutters",
|
||||
options = {'SKIP_SAVE'},
|
||||
default = False,
|
||||
)
|
||||
|
||||
# Boolean modifier properties.
|
||||
material_mode: bpy.props.EnumProperty(
|
||||
name = "Materials",
|
||||
description = "Method for setting materials on the new faces",
|
||||
items = (('INDEX', "Index Based", ("Set the material on new faces based on the order of the material slot lists. If a material doesn't exist on the\n"
|
||||
"modifier object, the face will use the same material slot or the first if the object doesn't have enough slots.")),
|
||||
('TRANSFER', "Transfer", ("Transfer materials from non-empty slots to the result mesh, adding new materials as necessary.\n"
|
||||
"For empty slots, fall back to using the same material index as the operand mesh."))),
|
||||
items = (('INDEX', "Index Based",
|
||||
("Set the material on new faces based on the order of the material slot lists. If a material doesn't exist on the\n"
|
||||
"modifier object, the face will use the same material slot or the first if the object doesn't have enough slots.")),
|
||||
('TRANSFER', "Transfer",
|
||||
("Transfer materials from non-empty slots to the result mesh, adding new materials as necessary.\n"
|
||||
"For empty slots, fall back to using the same material index as the operand mesh."))),
|
||||
default = 'INDEX',
|
||||
)
|
||||
use_self: bpy.props.BoolProperty(
|
||||
@@ -52,12 +66,24 @@ class ModifierProperties():
|
||||
default = 0.000001,
|
||||
)
|
||||
|
||||
# Built-in Methods.
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(cls, context)
|
||||
|
||||
|
||||
def draw(self, context):
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
|
||||
col = layout.column()
|
||||
col.prop(self, "flip")
|
||||
if self._unflippable:
|
||||
col.enabled = False
|
||||
|
||||
layout.separator()
|
||||
if prefs.solver == 'EXACT':
|
||||
layout.prop(self, "material_mode")
|
||||
layout.prop(self, "use_self")
|
||||
@@ -66,53 +92,86 @@ class ModifierProperties():
|
||||
layout.prop(self, "double_threshold")
|
||||
|
||||
|
||||
# Custom Methods.
|
||||
def _filter_objects(self, context) -> tuple[list, list]:
|
||||
"""Returns lists of cutters & canvases."""
|
||||
|
||||
canvases = [context.active_object]
|
||||
cutters = [obj for obj in context.selected_objects if obj != context.active_object]
|
||||
|
||||
# Flip.
|
||||
if self.flip:
|
||||
canvases, cutters = cutters, canvases
|
||||
|
||||
# Filter Canvases.
|
||||
canvases = filter_canvases(self, context, canvases)
|
||||
if len(canvases) == 0:
|
||||
self.report({'WARNING'}, "No valid canvases selected")
|
||||
return None, None
|
||||
|
||||
# Filter Cutters.
|
||||
cutters = filter_cutters(self, context, cutters, canvases)
|
||||
if len(cutters) == 0:
|
||||
self.report({'WARNING'}, "No valid cutters selected")
|
||||
return None, None
|
||||
|
||||
return canvases, cutters
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /brush_boolean/ ------------------------------ ####
|
||||
|
||||
class BrushBoolean(ModifierProperties):
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(cls, context)
|
||||
|
||||
class BrushBoolean(BooleanBase):
|
||||
|
||||
def invoke(self, context, event):
|
||||
# Abort if there are less than 2 selected objects.
|
||||
# Abort if there are less than 2 objects selected.
|
||||
if len(context.selected_objects) < 2:
|
||||
self.report({'WARNING'}, "Boolean operator needs at least two selected objects")
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Abort if active object is linked.
|
||||
if is_linked(context, context.active_object):
|
||||
self.report({'WARNING'}, "Boolean operators cannot be performed on linked objects")
|
||||
return {'CANCELLED'}
|
||||
if not self.flip:
|
||||
cutters = [obj for obj in context.selected_objects if obj != context.active_object]
|
||||
else:
|
||||
cutters = [context.active_object]
|
||||
|
||||
return self.execute(context)
|
||||
self._unflippable = False
|
||||
return convert_to_mesh_confirmation(self, context, event, cutters, "Brush Boolean")
|
||||
|
||||
|
||||
def execute(self, context):
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
canvas = context.active_object
|
||||
cutters = list_candidate_objects(self, context, context.active_object)
|
||||
|
||||
if len(cutters) == 0:
|
||||
# Create lists of cutters & canvases.
|
||||
canvases, cutters = self._filter_objects(context)
|
||||
if canvases is None or cutters is None:
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Create slices.
|
||||
if self.mode == "SLICE":
|
||||
for cutter in cutters:
|
||||
"""NOTE: Slices need to be created in a separate loop to avoid inheriting boolean modifiers that the operator adds."""
|
||||
slice = create_slice(context, canvas, modifier=True)
|
||||
add_boolean_modifier(self, context, slice, cutter, "INTERSECT", prefs.solver, pin=prefs.pin)
|
||||
"""
|
||||
NOTE: Slices need to be created in a separate loop to avoid
|
||||
inheriting Boolean modifiers that the operator adds.
|
||||
"""
|
||||
for canvas in canvases:
|
||||
slice = create_slice(context, canvas, modifier=True)
|
||||
add_boolean_modifier(self, context, slice, cutter, "INTERSECT", prefs.solver, pin=prefs.pin)
|
||||
|
||||
for cutter in cutters:
|
||||
mode = "DIFFERENCE" if self.mode == "SLICE" else self.mode
|
||||
display = 'WIRE' if prefs.wireframe else 'BOUNDS'
|
||||
set_cutter_properties(context, cutter, self.mode, display=display, collection=prefs.use_collection)
|
||||
add_boolean_modifier(self, context, canvas, cutter, mode, prefs.solver, pin=prefs.pin)
|
||||
if prefs.parent:
|
||||
change_parent(cutter, canvas)
|
||||
make_cutter(context, cutter, self.mode,
|
||||
display=prefs.display,
|
||||
collection=prefs.use_collection)
|
||||
|
||||
canvas.booleans.canvas = True
|
||||
mode = "DIFFERENCE" if self.mode == "SLICE" else self.mode
|
||||
for canvas in canvases:
|
||||
add_boolean_modifier(self, context, canvas, cutter, mode, prefs.solver, pin=prefs.pin)
|
||||
|
||||
if prefs.parent:
|
||||
change_parent(context, cutter, canvases[0], inverse=True)
|
||||
|
||||
# Set Boolean property to canvases.
|
||||
for canvas in canvases:
|
||||
canvas.booleans.canvas = True
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -120,29 +179,47 @@ class BrushBoolean(ModifierProperties):
|
||||
class OBJECT_OT_boolean_brush_union(bpy.types.Operator, BrushBoolean):
|
||||
bl_idname = "object.boolean_brush_union"
|
||||
bl_label = "Boolean Union (Brush)"
|
||||
bl_description = "Merge selected objects into active one"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
mode = "UNION"
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if not properties.flip:
|
||||
return "Merge selected objects into the active one"
|
||||
else:
|
||||
return "Merge the active object into selected ones"
|
||||
|
||||
|
||||
class OBJECT_OT_boolean_brush_intersect(bpy.types.Operator, BrushBoolean):
|
||||
bl_idname = "object.boolean_brush_intersect"
|
||||
bl_label = "Boolean Intersection (Brush)"
|
||||
bl_description = "Only keep the parts of the active object that are interesecting selected objects"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
mode = "INTERSECT"
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if not properties.flip:
|
||||
return "Only keep parts of the active object that are interesecting selected objects"
|
||||
else:
|
||||
return "Only keep parts of selected objects that are interesecting the active one"
|
||||
|
||||
|
||||
class OBJECT_OT_boolean_brush_difference(bpy.types.Operator, BrushBoolean):
|
||||
bl_idname = "object.boolean_brush_difference"
|
||||
bl_label = "Boolean Difference (Brush)"
|
||||
bl_description = "Subtract selected objects from active one"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
mode = "DIFFERENCE"
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if not properties.flip:
|
||||
return "Subtract selected objects from the active one"
|
||||
else:
|
||||
return "Subtract the active object from selected ones"
|
||||
|
||||
|
||||
class OBJECT_OT_boolean_brush_slice(bpy.types.Operator, BrushBoolean):
|
||||
bl_idname = "object.boolean_brush_slice"
|
||||
@@ -152,15 +229,18 @@ class OBJECT_OT_boolean_brush_slice(bpy.types.Operator, BrushBoolean):
|
||||
|
||||
mode = "SLICE"
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if not properties.flip:
|
||||
return "Slice the active object by selected ones. Will create slices as separate objects"
|
||||
else:
|
||||
return "Slice selected objects by the active one. Will create slices as separate objects"
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /auto_boolean/ ------------------------------ ####
|
||||
|
||||
class AutoBoolean(ModifierProperties):
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(cls, context)
|
||||
|
||||
class AutoBoolean(BooleanBase):
|
||||
|
||||
def invoke(self, context, event):
|
||||
# Abort if there are less than 2 selected objects.
|
||||
@@ -168,55 +248,66 @@ class AutoBoolean(ModifierProperties):
|
||||
self.report({'WARNING'}, "Boolean operator needs at least two selected objects")
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Abort if active object is linked.
|
||||
if is_linked(context, context.active_object):
|
||||
self.report({'ERROR'}, "Modifiers cannot be applied to linked object")
|
||||
return {'CANCELLED'}
|
||||
if not self.flip:
|
||||
canvases = [context.active_object]
|
||||
else:
|
||||
canvases = [obj for obj in context.selected_objects if obj != context.active_object]
|
||||
|
||||
return destructive_op_confirmation(self, context, event, [context.active_object], title="Auto Boolean")
|
||||
for canvas in canvases:
|
||||
if canvas.type != 'MESH':
|
||||
canvases.remove(canvas)
|
||||
|
||||
self._unflippable = False
|
||||
return destructive_op_confirmation(self, context, event, canvases, "Auto Boolean")
|
||||
|
||||
|
||||
def execute(self, context):
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
canvas = context.active_object
|
||||
cutters = list_candidate_objects(self, context, context.active_object)
|
||||
new_modifiers = defaultdict(list)
|
||||
|
||||
if len(cutters) == 0:
|
||||
# Create lists of cutters & canvases.
|
||||
canvases, cutters = self._filter_objects(context)
|
||||
if canvases is None or cutters is None:
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Create slices.
|
||||
if self.mode == "SLICE":
|
||||
for cutter in cutters:
|
||||
"""NOTE: Slices need to be created in a separate loop to avoid inheriting boolean modifiers that the operator adds."""
|
||||
slice = create_slice(context, canvas)
|
||||
modifier = add_boolean_modifier(self, context, slice, cutter, "INTERSECT", prefs.solver, pin=prefs.pin)
|
||||
new_modifiers[slice].append(modifier)
|
||||
slice.select_set(True)
|
||||
"""
|
||||
NOTE: Slices need to be created in a separate loop to avoid
|
||||
inheriting Boolean modifiers that the operator adds.
|
||||
"""
|
||||
for canvas in canvases:
|
||||
slice = create_slice(context, canvas)
|
||||
modifier = add_boolean_modifier(self, context, slice, cutter, "INTERSECT",
|
||||
prefs.solver, pin=prefs.pin)
|
||||
new_modifiers[slice].append(modifier)
|
||||
slice.select_set(True)
|
||||
|
||||
for cutter in cutters:
|
||||
# Add boolean modifier on canvas.
|
||||
# Add Boolean modifier on canvases.
|
||||
mode = "DIFFERENCE" if self.mode == "SLICE" else self.mode
|
||||
modifier = add_boolean_modifier(self, context, canvas, cutter, mode, prefs.solver, pin=prefs.pin)
|
||||
new_modifiers[canvas].append(modifier)
|
||||
for canvas in canvases:
|
||||
modifier = add_boolean_modifier(self, context, canvas, cutter, mode, prefs.solver, pin=prefs.pin)
|
||||
new_modifiers[canvas].append(modifier)
|
||||
|
||||
# Transfer cutters children to canvas.
|
||||
# Transfer cutters children to a canvas.
|
||||
for child in cutter.children:
|
||||
change_parent(child, canvas)
|
||||
change_parent(context, child, canvases[0])
|
||||
|
||||
# Select all faces of the cutter so that newly created faces in canvas
|
||||
# are also selected after applying the modifier.
|
||||
for face in cutter.data.polygons:
|
||||
face.select = True
|
||||
|
||||
# Apply modifiers on canvas & slices.
|
||||
# Apply modifiers on canvases & slices.
|
||||
for obj, modifiers in new_modifiers.items():
|
||||
modifiers = get_modifiers_to_apply(context, obj, modifiers)
|
||||
apply_modifiers(context, obj, modifiers)
|
||||
|
||||
# Delete cutters.
|
||||
for cutter in cutters:
|
||||
delete_cutter(cutter)
|
||||
delete_object(cutter)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -224,29 +315,47 @@ class AutoBoolean(ModifierProperties):
|
||||
class OBJECT_OT_boolean_auto_union(bpy.types.Operator, AutoBoolean):
|
||||
bl_idname = "object.boolean_auto_union"
|
||||
bl_label = "Boolean Union (Auto)"
|
||||
bl_description = "Merge selected objects into active one"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
mode = "UNION"
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if not properties.flip:
|
||||
return "Merge selected objects into the active one"
|
||||
else:
|
||||
return "Merge the active object into selected ones"
|
||||
|
||||
|
||||
class OBJECT_OT_boolean_auto_difference(bpy.types.Operator, AutoBoolean):
|
||||
bl_idname = "object.boolean_auto_difference"
|
||||
bl_label = "Boolean Difference (Auto)"
|
||||
bl_description = "Subtract selected objects from active one"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
mode = "DIFFERENCE"
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if not properties.flip:
|
||||
return "Subtract selected objects from the active one"
|
||||
else:
|
||||
return "Subtract the active object from selected ones"
|
||||
|
||||
|
||||
class OBJECT_OT_boolean_auto_intersect(bpy.types.Operator, AutoBoolean):
|
||||
bl_idname = "object.boolean_auto_intersect"
|
||||
bl_label = "Boolean Intersect (Auto)"
|
||||
bl_description = "Only keep the parts of the active object that are interesecting selected objects"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
mode = "INTERSECT"
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if not properties.flip:
|
||||
return "Only keep parts of the active object that are interesecting selected objects"
|
||||
else:
|
||||
return "Only keep parts of selected objects that are interesecting the active one"
|
||||
|
||||
|
||||
class OBJECT_OT_boolean_auto_slice(bpy.types.Operator, AutoBoolean):
|
||||
bl_idname = "object.boolean_auto_slice"
|
||||
@@ -256,13 +365,20 @@ class OBJECT_OT_boolean_auto_slice(bpy.types.Operator, AutoBoolean):
|
||||
|
||||
mode = "SLICE"
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if not properties.flip:
|
||||
return "Slice the active object by selected ones. Will create slices as separate objects"
|
||||
else:
|
||||
return "Slice selected objects by the active one. Will create slices as separate objects"
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
addon_keymaps = []
|
||||
|
||||
classes = [
|
||||
classes = (
|
||||
OBJECT_OT_boolean_brush_union,
|
||||
OBJECT_OT_boolean_brush_difference,
|
||||
OBJECT_OT_boolean_brush_intersect,
|
||||
@@ -272,7 +388,7 @@ classes = [
|
||||
OBJECT_OT_boolean_auto_difference,
|
||||
OBJECT_OT_boolean_auto_intersect,
|
||||
OBJECT_OT_boolean_auto_slice,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
|
||||
@@ -2,29 +2,30 @@ import bpy
|
||||
import itertools
|
||||
from .. import __package__ as base_package
|
||||
|
||||
from ..functions.poll import (
|
||||
basic_poll,
|
||||
is_canvas,
|
||||
is_instanced_data,
|
||||
destructive_op_confirmation,
|
||||
from ..functions.canvas import (
|
||||
list_selected_canvases,
|
||||
list_canvas_cutters,
|
||||
list_canvas_slices,
|
||||
)
|
||||
from ..functions.cutter import (
|
||||
list_cutter_users,
|
||||
handle_unused_cutters,
|
||||
)
|
||||
from ..functions.modifier import (
|
||||
apply_modifiers,
|
||||
get_modifiers_to_apply,
|
||||
is_boolean_modifier,
|
||||
)
|
||||
from ..functions.object import (
|
||||
object_visibility_set,
|
||||
delete_empty_collection,
|
||||
delete_cutter,
|
||||
change_parent,
|
||||
delete_object,
|
||||
)
|
||||
from ..functions.list import (
|
||||
list_canvases,
|
||||
list_canvas_slices,
|
||||
list_canvas_cutters,
|
||||
list_cutter_users,
|
||||
list_selected_canvases,
|
||||
list_unused_cutters,
|
||||
list_pre_boolean_modifiers,
|
||||
from ..functions.poll import (
|
||||
basic_poll,
|
||||
destructive_op_confirmation,
|
||||
_guess_toggle_state,
|
||||
)
|
||||
from ..functions.scene import (
|
||||
delete_empty_collection,
|
||||
)
|
||||
|
||||
|
||||
@@ -34,41 +35,50 @@ from ..functions.list import (
|
||||
class OBJECT_OT_boolean_toggle_all(bpy.types.Operator):
|
||||
bl_idname = "object.boolean_toggle_all"
|
||||
bl_label = "Toggle Boolean Cutters"
|
||||
bl_description = "Toggle all boolean cutters affecting selected canvases"
|
||||
bl_description = "Toggle all Boolean cutters affecting selected canvases"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(cls, context, check_linked=True) and is_canvas(context.active_object)
|
||||
return basic_poll(cls, context, check_active=False)
|
||||
|
||||
def execute(self, context):
|
||||
# Filter canvases.
|
||||
canvases = list_selected_canvases(context)
|
||||
if len(canvases) == 0:
|
||||
self.report({'WARNING'}, "No valid canvases selected")
|
||||
return {'CANCELLED'}
|
||||
|
||||
cutters, modifiers = list_canvas_cutters(canvases)
|
||||
slices = list_canvas_slices(canvases)
|
||||
modifiers = list(itertools.chain.from_iterable(modifiers.values()))
|
||||
slices = list_canvas_slices(context, canvases)
|
||||
|
||||
state = _guess_toggle_state(modifiers)
|
||||
state = True if state == "On" else False
|
||||
|
||||
# Toggle Modifiers
|
||||
for mod in modifiers:
|
||||
mod.show_viewport = not mod.show_viewport
|
||||
mod.show_render = not mod.show_render
|
||||
mod.show_viewport = not state
|
||||
mod.show_render = not state
|
||||
|
||||
# Hide Slices
|
||||
for slice in slices:
|
||||
slice.hide_viewport = not slice.hide_viewport
|
||||
slice.hide_render = not slice.hide_render
|
||||
slice.hide_viewport = state
|
||||
slice.hide_render = state
|
||||
slice.hide_set(state)
|
||||
for mod in slice.modifiers:
|
||||
if mod.type == 'BOOLEAN' and mod.object in cutters:
|
||||
mod.show_viewport = not mod.show_viewport
|
||||
mod.show_render = not mod.show_render
|
||||
if not is_boolean_modifier(mod):
|
||||
continue
|
||||
if mod.object in cutters:
|
||||
mod.show_viewport = not state
|
||||
mod.show_render = not state
|
||||
|
||||
# Hide Unused Cutters
|
||||
other_canvases = list_canvases()
|
||||
for obj in other_canvases:
|
||||
if obj not in canvases + slices:
|
||||
if any(mod.object in cutters and mod.show_viewport for mod in obj.modifiers if mod.type == 'BOOLEAN'):
|
||||
cutters[:] = [cutter for cutter in cutters if cutter not in [mod.object for mod in obj.modifiers]]
|
||||
|
||||
for cutter in cutters:
|
||||
cutter.hide_viewport = not cutter.hide_viewport
|
||||
other_canvases = list_cutter_users([cutter], exclude=canvases + slices).keys()
|
||||
if len(other_canvases) == 0:
|
||||
cutter.hide_viewport = state
|
||||
cutter.hide_set(state)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -77,72 +87,49 @@ class OBJECT_OT_boolean_toggle_all(bpy.types.Operator):
|
||||
class OBJECT_OT_boolean_remove_all(bpy.types.Operator):
|
||||
bl_idname = "object.boolean_remove_all"
|
||||
bl_label = "Remove Boolean Cutters"
|
||||
bl_description = "Remove all boolean cutters from selected canvases"
|
||||
bl_options = {'UNDO'}
|
||||
bl_description = "Remove all Boolean cutters from selected canvases"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
delete_cutters: bpy.props.BoolProperty(
|
||||
name = "Delete Unused Cutters",
|
||||
description = "Completely remove cutters if they're not used by any other remaining canvas",
|
||||
default = True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(cls, context, check_linked=True) and is_canvas(context.active_object)
|
||||
return basic_poll(cls, context, check_active=False)
|
||||
|
||||
def execute(self, context):
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
|
||||
# Filter canvases.
|
||||
canvases = list_selected_canvases(context)
|
||||
cutters, __ = list_canvas_cutters(canvases)
|
||||
slices = list_canvas_slices(canvases)
|
||||
if len(canvases) == 0:
|
||||
self.report({'WARNING'}, "No valid canvases selected")
|
||||
return {'CANCELLED'}
|
||||
|
||||
cutters, modifiers = list_canvas_cutters(canvases)
|
||||
slices = list_canvas_slices(context, canvases)
|
||||
|
||||
# Remove Slices
|
||||
for slice in slices:
|
||||
if slice in canvases:
|
||||
canvases.remove(slice)
|
||||
delete_cutter(slice)
|
||||
delete_object(slice)
|
||||
|
||||
for canvas in canvases:
|
||||
# Remove Modifiers
|
||||
for mod in canvas.modifiers:
|
||||
if mod.type == 'BOOLEAN' and "boolean_" in mod.name:
|
||||
if mod.object in cutters:
|
||||
canvas.modifiers.remove(mod)
|
||||
# Remove Modifiers
|
||||
for canvas, mods in modifiers.items():
|
||||
for mod in mods:
|
||||
canvas.modifiers.remove(mod)
|
||||
canvas.booleans.canvas = False
|
||||
|
||||
# remove_boolean_properties
|
||||
if canvas.booleans.canvas == True:
|
||||
canvas.booleans.canvas = False
|
||||
# Handle Unused Cutters
|
||||
handle_unused_cutters(context, list(cutters), canvases + slices,
|
||||
delete=self.delete_cutters)
|
||||
|
||||
|
||||
# Restore Orphaned Cutters
|
||||
unused_cutters, leftovers = list_unused_cutters(cutters, canvases, slices, do_leftovers=True)
|
||||
|
||||
for cutter in unused_cutters:
|
||||
if cutter.booleans.carver:
|
||||
delete_cutter(cutter)
|
||||
else:
|
||||
# restore_visibility
|
||||
cutter.hide_render = False
|
||||
cutter.display_type = 'TEXTURED'
|
||||
cutter.lineart.usage = 'INHERIT'
|
||||
object_visibility_set(cutter, value=True)
|
||||
cutter.booleans.cutter = ""
|
||||
|
||||
# remove_parent_&_collection
|
||||
if prefs.parent and cutter.parent in canvases:
|
||||
change_parent(cutter, None)
|
||||
|
||||
if prefs.use_collection:
|
||||
cutters_collection = bpy.data.collections.get(prefs.collection_name)
|
||||
if cutters_collection in cutter.users_collection:
|
||||
bpy.data.collections.get(prefs.collection_name).objects.unlink(cutter)
|
||||
|
||||
# purge_empty_collection
|
||||
if prefs.use_collection:
|
||||
delete_empty_collection()
|
||||
|
||||
|
||||
# Change Leftover Cutter Parent
|
||||
if prefs.parent:
|
||||
for cutter in leftovers:
|
||||
if cutter.parent in canvases:
|
||||
other_canvases = list_cutter_users([cutter])
|
||||
change_parent(cutter, other_canvases[0])
|
||||
# Purge Empty Collection
|
||||
delete_empty_collection(context)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -151,74 +138,56 @@ class OBJECT_OT_boolean_remove_all(bpy.types.Operator):
|
||||
class OBJECT_OT_boolean_apply_all(bpy.types.Operator):
|
||||
bl_idname = "object.boolean_apply_all"
|
||||
bl_label = "Apply All Boolean Cutters"
|
||||
bl_description = "Apply all boolean cutters on selected canvases"
|
||||
bl_options = {'UNDO'}
|
||||
bl_description = "Apply all Boolean cutters to selected canvases"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
delete_cutters: bpy.props.BoolProperty(
|
||||
name = "Delete Unused Cutters",
|
||||
description = "Completely remove cutters if they're not used by any other remaining canvas",
|
||||
default = True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(cls, context, check_linked=True) and is_canvas(context.active_object)
|
||||
|
||||
return basic_poll(cls, context, check_active=False)
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.canvases = list_selected_canvases(context)
|
||||
return destructive_op_confirmation(self, context, event, self.canvases, title="Apply Boolean Cutters")
|
||||
# Filter canvases.
|
||||
canvases = list_selected_canvases(context)
|
||||
if len(canvases) == 0:
|
||||
self.report({'WARNING'}, "No valid canvases selected")
|
||||
return {'CANCELLED'}
|
||||
|
||||
return destructive_op_confirmation(self, context, event, canvases, title="Apply Boolean Cutters")
|
||||
|
||||
def execute(self, context):
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
|
||||
cutters, __ = list_canvas_cutters(self.canvases)
|
||||
slices = list_canvas_slices(self.canvases)
|
||||
canvases = list_selected_canvases(context)
|
||||
cutters, __ = list_canvas_cutters(canvases)
|
||||
slices = list_canvas_slices(context, canvases)
|
||||
|
||||
# Select all faces of the cutter so that newly created faces in canvas
|
||||
# are also selected after applying the modifier.
|
||||
for cutter in cutters:
|
||||
for cutter in list(cutters):
|
||||
for face in cutter.data.polygons:
|
||||
face.select = True
|
||||
|
||||
for canvas in itertools.chain(self.canvases, slices):
|
||||
context.view_layer.objects.active = canvas
|
||||
|
||||
for canvas in itertools.chain(canvases, slices):
|
||||
# Apply Modifiers
|
||||
if prefs.apply_order == 'ALL':
|
||||
modifiers = [mod for mod in canvas.modifiers]
|
||||
elif prefs.apply_order == 'BEFORE':
|
||||
modifiers = list_pre_boolean_modifiers(canvas)
|
||||
elif prefs.apply_order == 'BOOLEANS':
|
||||
modifiers = [mod for mod in canvas.modifiers if mod.type == 'BOOLEAN' and "boolean_" in mod.name]
|
||||
|
||||
modifiers = get_modifiers_to_apply(context, canvas)
|
||||
apply_modifiers(context, canvas, modifiers)
|
||||
|
||||
# remove_boolean_properties
|
||||
# Remove Boolean Properties
|
||||
canvas.booleans.canvas = False
|
||||
canvas.booleans.slice = False
|
||||
canvas.booleans.slice_of = None
|
||||
|
||||
# Handle Unused Cutters
|
||||
handle_unused_cutters(context, list(cutters), canvases, delete=self.delete_cutters)
|
||||
|
||||
# Purge Orphaned Cutters
|
||||
unused_cutters, leftovers = list_unused_cutters(cutters, self.canvases, slices, do_leftovers=True)
|
||||
|
||||
purged_cutters = []
|
||||
for cutter in unused_cutters:
|
||||
if cutter not in purged_cutters:
|
||||
# Transfer Children
|
||||
for child in cutter.children:
|
||||
change_parent(child, cutter.parent)
|
||||
|
||||
# Purge
|
||||
delete_cutter(cutter)
|
||||
purged_cutters.append(cutter)
|
||||
|
||||
# purge_empty_collection
|
||||
if prefs.use_collection:
|
||||
delete_empty_collection()
|
||||
|
||||
|
||||
# Change Leftover Cutter Parent
|
||||
if prefs.parent:
|
||||
for cutter in leftovers:
|
||||
if cutter.parent in self.canvases:
|
||||
other_canvases = list_cutter_users([cutter])
|
||||
change_parent(cutter, other_canvases[0])
|
||||
# Purge Empty Collection
|
||||
delete_empty_collection(context)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -228,11 +197,11 @@ class OBJECT_OT_boolean_apply_all(bpy.types.Operator):
|
||||
|
||||
addon_keymaps = []
|
||||
|
||||
classes = [
|
||||
classes = (
|
||||
OBJECT_OT_boolean_toggle_all,
|
||||
OBJECT_OT_boolean_remove_all,
|
||||
OBJECT_OT_boolean_apply_all,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
|
||||
@@ -1,27 +1,35 @@
|
||||
import bpy
|
||||
import itertools
|
||||
from .. import __package__ as base_package
|
||||
|
||||
from ..functions.poll import (
|
||||
basic_poll,
|
||||
is_instanced_data,
|
||||
destructive_op_confirmation,
|
||||
from ..functions.canvas import (
|
||||
list_canvas_cutters,
|
||||
list_canvas_slices,
|
||||
)
|
||||
from ..functions.cutter import (
|
||||
list_selected_cutters,
|
||||
list_cutter_users,
|
||||
restore_cutter,
|
||||
handle_unused_cutters,
|
||||
)
|
||||
from ..functions.modifier import (
|
||||
apply_modifiers,
|
||||
is_boolean_modifier,
|
||||
)
|
||||
from ..functions.object import (
|
||||
object_visibility_set,
|
||||
delete_empty_collection,
|
||||
delete_cutter,
|
||||
change_parent,
|
||||
delete_object,
|
||||
)
|
||||
from ..functions.list import (
|
||||
list_canvases,
|
||||
list_selected_cutters,
|
||||
list_canvas_cutters,
|
||||
list_canvas_slices,
|
||||
list_cutter_users,
|
||||
list_unused_cutters,
|
||||
from ..functions.poll import (
|
||||
basic_poll,
|
||||
destructive_op_confirmation,
|
||||
_guess_toggle_state,
|
||||
)
|
||||
from ..functions.scene import (
|
||||
delete_empty_collection,
|
||||
)
|
||||
from ..ui.common import (
|
||||
preserve_list_index,
|
||||
)
|
||||
|
||||
|
||||
@@ -31,13 +39,19 @@ from ..functions.list import (
|
||||
class OBJECT_OT_boolean_toggle_cutter(bpy.types.Operator):
|
||||
bl_idname = "object.boolean_toggle_cutter"
|
||||
bl_label = "Toggle Boolean Cutter"
|
||||
bl_description = "Toggle this boolean cutter. If cutter is the active object it will be toggled for every canvas that uses it"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if properties.specified_cutter:
|
||||
return ("Toggle selected cutter and its effect")
|
||||
else:
|
||||
return ("Toggle all selected cutters and their effects")
|
||||
|
||||
method: bpy.props.EnumProperty(
|
||||
name = "Method",
|
||||
items = (('ALL', "All", "Remove cutter from all canvases that use it"),
|
||||
('SPECIFIED', "Specified", "Remove cutter from specified canvas")),
|
||||
items = (('ALL', "All", "Toggle all selected cutters"),
|
||||
('SPECIFIED', "Specified", "Toggle selected cutter")),
|
||||
default = 'ALL',
|
||||
)
|
||||
|
||||
@@ -45,57 +59,71 @@ class OBJECT_OT_boolean_toggle_cutter(bpy.types.Operator):
|
||||
)
|
||||
specified_canvas: bpy.props.StringProperty(
|
||||
)
|
||||
specified_modifier: bpy.props.StringProperty(
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(cls, context, check_linked=True)
|
||||
return basic_poll(cls, context, check_active=False)
|
||||
|
||||
def execute(self, context):
|
||||
# Create lists of cutters & canvases.
|
||||
if self.method == 'SPECIFIED':
|
||||
canvases = [context.scene.objects[self.specified_canvas]]
|
||||
cutters = [context.scene.objects[self.specified_cutter]]
|
||||
slices = list_canvas_slices(canvases)
|
||||
cutters: list = [context.scene.objects[self.specified_cutter]]
|
||||
canvases: list = [context.scene.objects[self.specified_canvas]]
|
||||
modifiers: list = [canvases[0].modifiers.get(self.specified_modifier)]
|
||||
slices: list = list_canvas_slices(context, canvases)
|
||||
elif self.method == 'ALL':
|
||||
cutters = list_selected_cutters(context)
|
||||
canvases = list_cutter_users(cutters)
|
||||
cutters: list = list_selected_cutters(context)
|
||||
canvases: dict = list_cutter_users(cutters)
|
||||
modifiers: list = list(itertools.chain.from_iterable(canvases.values()))
|
||||
|
||||
if cutters:
|
||||
for canvas in canvases:
|
||||
# toggle_slices_visibility (for_all_canvases)
|
||||
if canvas.booleans.slice == True:
|
||||
if any(modifier.object in cutters for modifier in canvas.modifiers):
|
||||
canvas.hide_viewport = not canvas.hide_viewport
|
||||
canvas.hide_render = not canvas.hide_render
|
||||
|
||||
# Toggle Modifiers
|
||||
for mod in canvas.modifiers:
|
||||
if mod.type == 'BOOLEAN' and mod.object in cutters:
|
||||
mod.show_viewport = not mod.show_viewport
|
||||
mod.show_render = not mod.show_render
|
||||
|
||||
|
||||
if self.method == 'SPECIFIED':
|
||||
# toggle_slices_visibility (for_specified_canvas)
|
||||
for slice in slices:
|
||||
for mod in slice.modifiers:
|
||||
if mod.type == 'BOOLEAN' and mod.object in cutters:
|
||||
slice.hide_viewport = not slice.hide_viewport
|
||||
slice.hide_render = not slice.hide_render
|
||||
mod.show_viewport = not mod.show_viewport
|
||||
mod.show_render = not mod.show_render
|
||||
|
||||
# hide_cutter_if_not_used_by_any_visible_modifiers
|
||||
other_canvases = list_canvases()
|
||||
for obj in other_canvases:
|
||||
if obj not in canvases + slices:
|
||||
if any(mod.object in cutters and mod.show_viewport for mod in obj.modifiers if mod.type == 'BOOLEAN'):
|
||||
cutters[:] = [cutter for cutter in cutters if cutter not in [mod.object for mod in obj.modifiers]]
|
||||
|
||||
for cutter in cutters:
|
||||
cutter.hide_viewport = not cutter.hide_viewport
|
||||
|
||||
else:
|
||||
if len(cutters) == 0:
|
||||
self.report({'INFO'}, "Boolean cutters are not selected")
|
||||
return {'CANCELLED'}
|
||||
|
||||
if not canvases:
|
||||
return {'CANCELLED'}
|
||||
|
||||
state = _guess_toggle_state(modifiers)
|
||||
state = True if state == "On" else False
|
||||
|
||||
# Toggle Modifiers
|
||||
for mod in modifiers:
|
||||
mod.show_viewport = not state
|
||||
mod.show_render = not state
|
||||
|
||||
# Hide Slices
|
||||
if self.method == 'ALL':
|
||||
for canvas in canvases.keys():
|
||||
if not canvas.booleans.slice:
|
||||
continue
|
||||
|
||||
slice = canvas
|
||||
if any(modifier.object in cutters for modifier in slice.modifiers):
|
||||
slice.hide_viewport = state
|
||||
slice.hide_render = state
|
||||
slice.hide_set(state)
|
||||
|
||||
elif self.method == 'SPECIFIED':
|
||||
for slice in slices:
|
||||
for mod in slice.modifiers:
|
||||
if not is_boolean_modifier(mod):
|
||||
continue
|
||||
if mod.object in cutters:
|
||||
mod.show_viewport = not state
|
||||
mod.show_render = not state
|
||||
|
||||
slice.hide_viewport = state
|
||||
slice.hide_render = state
|
||||
slice.hide_set(state)
|
||||
|
||||
# Hide Unused Cutters
|
||||
for cutter in cutters:
|
||||
other_canvases = list_cutter_users([cutter], exclude=canvases + slices).keys()
|
||||
if len(other_canvases) == 0:
|
||||
cutter.hide_viewport = state
|
||||
cutter.hide_set(state)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -104,9 +132,16 @@ class OBJECT_OT_boolean_toggle_cutter(bpy.types.Operator):
|
||||
class OBJECT_OT_boolean_remove_cutter(bpy.types.Operator):
|
||||
bl_idname = "object.boolean_remove_cutter"
|
||||
bl_label = "Remove Boolean Cutter"
|
||||
bl_description = "Remove this boolean cutter. If cutter is the active object it will be removed from every canvas that uses it"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if properties.specified_cutter:
|
||||
return ("Remove this cutter and the Boolean modifier that uses it.\n"
|
||||
"If the cutter is not used by any other canvas it will be deleted from the scene")
|
||||
else:
|
||||
return ("Remove selected Boolean cutters from all canvases that uses them; restore their visibility")
|
||||
|
||||
method: bpy.props.EnumProperty(
|
||||
name = "Method",
|
||||
items = (('ALL', "All", "Remove cutter from all canvases that use it"),
|
||||
@@ -118,86 +153,70 @@ class OBJECT_OT_boolean_remove_cutter(bpy.types.Operator):
|
||||
)
|
||||
specified_canvas: bpy.props.StringProperty(
|
||||
)
|
||||
specified_modifier: bpy.props.StringProperty(
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(cls, context, check_linked=True)
|
||||
return basic_poll(cls, context, check_active=False)
|
||||
|
||||
def execute(self, context):
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
leftovers = []
|
||||
|
||||
# Create lists of cutters & canvases.
|
||||
if self.method == 'SPECIFIED':
|
||||
canvases = [context.scene.objects[self.specified_canvas]]
|
||||
cutters = [context.scene.objects[self.specified_cutter]]
|
||||
slices = list_canvas_slices(canvases)
|
||||
cutters: list = [context.scene.objects[self.specified_cutter]]
|
||||
canvases: dict = {
|
||||
context.scene.objects[self.specified_canvas]:
|
||||
[context.scene.objects[self.specified_canvas].modifiers.get(self.specified_modifier)]
|
||||
}
|
||||
elif self.method == 'ALL':
|
||||
cutters = list_selected_cutters(context)
|
||||
canvases = list_cutter_users(cutters)
|
||||
cutters: list = list_selected_cutters(context)
|
||||
canvases: dict = list_cutter_users(cutters)
|
||||
|
||||
if cutters:
|
||||
# Remove Modifiers
|
||||
for canvas in canvases:
|
||||
for mod in canvas.modifiers:
|
||||
if "boolean_" in mod.name:
|
||||
if mod.object in cutters:
|
||||
canvas.modifiers.remove(mod)
|
||||
|
||||
# remove_canvas_property_if_needed
|
||||
other_cutters, __ = list_canvas_cutters([canvas])
|
||||
if len(other_cutters) == 0:
|
||||
canvas.booleans.canvas = False
|
||||
|
||||
# Remove Slices (for_all_method)
|
||||
if canvas.booleans.slice == True:
|
||||
delete_cutter(canvas)
|
||||
|
||||
|
||||
if self.method == 'SPECIFIED':
|
||||
# Remove Slices (for_specified_method)
|
||||
for slice in slices:
|
||||
for mod in slice.modifiers:
|
||||
if mod.type == 'BOOLEAN' and mod.object in cutters:
|
||||
delete_cutter(slice)
|
||||
|
||||
cutters, leftovers = list_unused_cutters(cutters, canvases, do_leftovers=True)
|
||||
|
||||
|
||||
# Restore Orphaned Cutters
|
||||
for cutter in cutters:
|
||||
if self.method == 'SPECIFIED' and cutter.booleans.carver:
|
||||
delete_cutter(cutter)
|
||||
else:
|
||||
# restore_visibility
|
||||
cutter.hide_render = False
|
||||
cutter.display_type = 'TEXTURED'
|
||||
cutter.lineart.usage = 'INHERIT'
|
||||
object_visibility_set(cutter, value=True)
|
||||
cutter.booleans.cutter = ""
|
||||
|
||||
# remove_parent_&_collection
|
||||
if prefs.parent and cutter.parent in canvases:
|
||||
change_parent(cutter, None)
|
||||
|
||||
if prefs.use_collection:
|
||||
cutters_collection = bpy.data.collections.get(prefs.collection_name)
|
||||
if cutters_collection in cutter.users_collection:
|
||||
bpy.data.collections.get(prefs.collection_name).objects.unlink(cutter)
|
||||
|
||||
# purge_empty_collection
|
||||
if prefs.use_collection:
|
||||
delete_empty_collection()
|
||||
|
||||
|
||||
# Change Leftover Cutter Parent
|
||||
if prefs.parent and leftovers != None:
|
||||
for cutter in leftovers:
|
||||
if cutter.parent in canvases:
|
||||
other_canvases = list_cutter_users([cutter])
|
||||
change_parent(cutter, other_canvases[0])
|
||||
|
||||
else:
|
||||
if len(cutters) == 0:
|
||||
self.report({'INFO'}, "Boolean cutters are not selected")
|
||||
return {'CANCELLED'}
|
||||
|
||||
if not canvases:
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Remove Slices
|
||||
slices = list_canvas_slices(context, canvases.keys())
|
||||
for slice in slices:
|
||||
for mod in slice.modifiers:
|
||||
if not is_boolean_modifier(mod):
|
||||
continue
|
||||
if mod.object in cutters:
|
||||
if slice in canvases:
|
||||
del canvases[slice]
|
||||
delete_object(slice)
|
||||
continue
|
||||
|
||||
# Remove Modifiers
|
||||
for canvas, modifiers in canvases.items():
|
||||
for mod in modifiers:
|
||||
with preserve_list_index(canvas.booleans, "modifiers_list_index"):
|
||||
canvas.modifiers.remove(mod)
|
||||
|
||||
# Unset canvas property if it's no longer needed.
|
||||
other_cutters, __ = list_canvas_cutters([canvas])
|
||||
if len(other_cutters) == 0:
|
||||
canvas.booleans.canvas = False
|
||||
|
||||
# Handle Unused Cutters
|
||||
if self.method == 'SPECIFIED':
|
||||
handle_unused_cutters(context, cutters, list(canvases.keys()), delete=True)
|
||||
|
||||
elif self.method == 'ALL':
|
||||
for cutter in cutters:
|
||||
# Restore unused cutters, even if they don't have other users.
|
||||
restore_cutter(context, cutter,
|
||||
unparent=prefs.parent and cutter.parent in canvases,
|
||||
unlink_collection=prefs.use_collection)
|
||||
|
||||
# Purge Empty Collection
|
||||
delete_empty_collection(context)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -206,104 +225,100 @@ class OBJECT_OT_boolean_remove_cutter(bpy.types.Operator):
|
||||
class OBJECT_OT_boolean_apply_cutter(bpy.types.Operator):
|
||||
bl_idname = "object.boolean_apply_cutter"
|
||||
bl_label = "Apply Boolean Cutter"
|
||||
bl_description = "Apply this boolean cutter. If cutter is the active object it will be applied to every canvas that uses it"
|
||||
bl_options = {'UNDO'}
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if properties.specified_cutter:
|
||||
return ("Apply the modifier using this Boolean cutter")
|
||||
else:
|
||||
return ("Apply this Boolean cutter to every canvas object that uses it")
|
||||
|
||||
method: bpy.props.EnumProperty(
|
||||
name = "Method",
|
||||
items = (('ALL', "All", "Remove cutter from all canvases that use it"),
|
||||
('SPECIFIED', "Specified", "Remove cutter from specified canvas")),
|
||||
items = (('ALL', "All", "Apply all selected cutter to all canvases that use them"),
|
||||
('SPECIFIED', "Specified", "Apply selected cutter to a specified canvas")),
|
||||
options = {'SKIP_SAVE', 'HIDDEN'},
|
||||
default = 'ALL',
|
||||
)
|
||||
|
||||
specified_cutter: bpy.props.StringProperty(
|
||||
options = {'SKIP_SAVE', 'HIDDEN'},
|
||||
)
|
||||
specified_canvas: bpy.props.StringProperty(
|
||||
options = {'SKIP_SAVE', 'HIDDEN'},
|
||||
)
|
||||
|
||||
delete: bpy.props.BoolProperty(
|
||||
name = "Delete Unused Cutter",
|
||||
description = "Completely remove the cutter object if it's not used by any other canvas",
|
||||
default = True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(cls, context, check_linked=True)
|
||||
|
||||
return basic_poll(cls, context, check_active=False)
|
||||
|
||||
def invoke(self, context, event):
|
||||
# Filter Objects
|
||||
if self.method == 'SPECIFIED':
|
||||
self.cutters = [context.scene.objects[self.specified_cutter]]
|
||||
self.canvases = [context.scene.objects[self.specified_canvas]]
|
||||
self.slices = list_canvas_slices(self.canvases)
|
||||
|
||||
canvases = [context.scene.objects[self.specified_canvas]]
|
||||
elif self.method == 'ALL':
|
||||
self.cutters = list_selected_cutters(context)
|
||||
self.canvases = list_cutter_users(self.cutters)
|
||||
|
||||
return destructive_op_confirmation(self, context, event, self.canvases, title="Apply Boolean Cutter")
|
||||
cutters = list_selected_cutters(context)
|
||||
canvases = list_cutter_users(cutters).keys()
|
||||
|
||||
return destructive_op_confirmation(self, context, event, canvases, title="Apply Boolean Cutter")
|
||||
|
||||
def execute(self, context):
|
||||
prefs = bpy.context.preferences.addons[base_package].preferences
|
||||
leftovers = []
|
||||
|
||||
if self.cutters:
|
||||
# Select all faces of the cutter so that newly created faces in canvas
|
||||
# are also selected after applying the modifier.
|
||||
for cutter in self.cutters:
|
||||
for face in cutter.data.polygons:
|
||||
face.select = True
|
||||
# Create lists of cutters & canvases.
|
||||
if self.method == 'SPECIFIED':
|
||||
cutters = [context.scene.objects[self.specified_cutter]]
|
||||
canvases = [context.scene.objects[self.specified_canvas]]
|
||||
slices = list_canvas_slices(context, canvases)
|
||||
elif self.method == 'ALL':
|
||||
cutters = list_selected_cutters(context)
|
||||
canvases = list_cutter_users(cutters).keys()
|
||||
slices = []
|
||||
|
||||
# Apply Modifiers
|
||||
for canvas in self.canvases:
|
||||
context.view_layer.objects.active = canvas
|
||||
if len(cutters) == 0:
|
||||
self.report({'INFO'}, "Boolean cutters are not selected")
|
||||
return {'CANCELLED'}
|
||||
|
||||
boolean_mods = []
|
||||
for mod in canvas.modifiers:
|
||||
if "boolean_" in mod.name:
|
||||
if mod.object in self.cutters:
|
||||
boolean_mods.append(mod)
|
||||
apply_modifiers(context, canvas, boolean_mods)
|
||||
if not canvases:
|
||||
return {'CANCELLED'}
|
||||
|
||||
# remove_canvas_property_if_needed
|
||||
# Select all faces of the cutter so that newly created faces in canvas
|
||||
# are also selected after applying the modifier.
|
||||
for cutter in cutters:
|
||||
for face in cutter.data.polygons:
|
||||
face.select = True
|
||||
|
||||
# Apply Modifiers
|
||||
for canvas in itertools.chain(canvases, slices):
|
||||
boolean_mods = []
|
||||
for mod in canvas.modifiers:
|
||||
if not is_boolean_modifier(mod):
|
||||
continue
|
||||
if mod.object in cutters:
|
||||
boolean_mods.append(mod)
|
||||
|
||||
if boolean_mods:
|
||||
with preserve_list_index(canvas.booleans, "modifiers_list_index"):
|
||||
apply_modifiers(context, canvas, boolean_mods)
|
||||
|
||||
# Unset canvas property if it's no longer needed.
|
||||
other_cutters, __ = list_canvas_cutters([canvas])
|
||||
if len(other_cutters) == 0:
|
||||
canvas.booleans.canvas = False
|
||||
canvas.booleans.slice = False
|
||||
canvas.booleans.slice_of = None
|
||||
|
||||
# Handle Unused Cutters
|
||||
handle_unused_cutters(context, cutters, canvases, delete=self.delete)
|
||||
|
||||
if self.method == 'SPECIFIED':
|
||||
# Apply Modifier for Slices (for_specified_method)
|
||||
for slice in self.slices:
|
||||
boolean_mods = []
|
||||
for mod in slice.modifiers:
|
||||
if mod.type == 'BOOLEAN' and mod.object in self.cutters:
|
||||
boolean_mods.append(mod)
|
||||
apply_modifiers(context, slice, boolean_mods)
|
||||
|
||||
|
||||
unused_cutters, leftovers = list_unused_cutters(self.cutters, self.canvases, do_leftovers=True)
|
||||
|
||||
|
||||
for cutter in unused_cutters:
|
||||
# Transfer Children
|
||||
for child in cutter.children:
|
||||
change_parent(child, cutter.parent)
|
||||
|
||||
# Purge Orphaned Cutters
|
||||
delete_cutter(cutter)
|
||||
|
||||
# purge_empty_collection
|
||||
if prefs.use_collection:
|
||||
delete_empty_collection()
|
||||
|
||||
|
||||
# Change Leftover Cutter Parent
|
||||
if prefs.parent and leftovers != None:
|
||||
for cutter in leftovers:
|
||||
if cutter.parent in self.canvases:
|
||||
other_canvases = list_cutter_users([cutter])
|
||||
change_parent(cutter, other_canvases[0])
|
||||
|
||||
else:
|
||||
self.report({'INFO'}, "Boolean cutters are not selected")
|
||||
# Purge Empty Collection
|
||||
delete_empty_collection(context)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -313,11 +328,11 @@ class OBJECT_OT_boolean_apply_cutter(bpy.types.Operator):
|
||||
|
||||
addon_keymaps = []
|
||||
|
||||
classes = [
|
||||
classes = (
|
||||
OBJECT_OT_boolean_toggle_cutter,
|
||||
OBJECT_OT_boolean_remove_cutter,
|
||||
OBJECT_OT_boolean_apply_cutter,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import bpy
|
||||
from .. import __package__ as base_package
|
||||
|
||||
from ..functions.poll import (
|
||||
basic_poll,
|
||||
active_modifier_poll,
|
||||
from ..functions.canvas import (
|
||||
is_canvas,
|
||||
)
|
||||
from ..functions.list import (
|
||||
list_selected_cutters,
|
||||
list_selected_canvases,
|
||||
list_canvas_cutters,
|
||||
)
|
||||
from ..functions.cutter import (
|
||||
list_selected_cutters,
|
||||
list_cutter_users,
|
||||
)
|
||||
from ..functions.poll import (
|
||||
basic_poll,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ OPERATORS ------------------------------ ####
|
||||
@@ -20,21 +20,24 @@ from ..functions.list import (
|
||||
class OBJECT_OT_select_cutter_canvas(bpy.types.Operator):
|
||||
bl_idname = "object.select_cutter_canvas"
|
||||
bl_label = "Select Boolean Canvas"
|
||||
bl_description = "Select all the objects that use selected objects as boolean cutters"
|
||||
bl_description = "Select all canvases that use selected objects as Boolean cutters"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(cls, context) and context.active_object.booleans.cutter
|
||||
return basic_poll(cls, context, check_active=True) and context.active_object.booleans.cutter
|
||||
|
||||
def execute(self, context):
|
||||
cutters = list_selected_cutters(context)
|
||||
canvases = list_cutter_users(cutters)
|
||||
canvases = list_cutter_users(cutters).keys()
|
||||
|
||||
# Select Canvases
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
for obj in context.scene.objects:
|
||||
obj.select_set(False)
|
||||
|
||||
# Select canvases.
|
||||
for canvas in canvases:
|
||||
canvas.select_set(True)
|
||||
if not canvas.booleans.slice:
|
||||
canvas.select_set(True)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -43,19 +46,21 @@ class OBJECT_OT_select_cutter_canvas(bpy.types.Operator):
|
||||
class OBJECT_OT_boolean_select_all(bpy.types.Operator):
|
||||
bl_idname = "object.boolean_select_all"
|
||||
bl_label = "Select Boolean Cutters"
|
||||
bl_description = "Select all boolean cutters affecting active object"
|
||||
bl_description = "Select all Boolean cutters affecting selected canvases"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(cls, context) and is_canvas(context.active_object)
|
||||
return basic_poll(cls, context, check_active=True) and is_canvas(context.active_object)
|
||||
|
||||
def execute(self, context):
|
||||
canvases = list_selected_canvases(context)
|
||||
cutters, __ = list_canvas_cutters(canvases)
|
||||
|
||||
# select_cutters
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
for obj in context.scene.objects:
|
||||
obj.select_set(False)
|
||||
|
||||
# Select cutters.
|
||||
for cutter in cutters:
|
||||
cutter.select_set(True)
|
||||
|
||||
@@ -66,24 +71,24 @@ class OBJECT_OT_boolean_select_all(bpy.types.Operator):
|
||||
class OBJECT_OT_boolean_select_cutter(bpy.types.Operator):
|
||||
bl_idname = "object.boolean_select_cutter"
|
||||
bl_label = "Select Boolean Cutter"
|
||||
bl_description = "Select object that is used as boolean cutter by this modifier"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
return (basic_poll(cls, context) and active_modifier_poll(context.active_object) and
|
||||
context.area.type == 'PROPERTIES' and context.space_data.context == 'MODIFIER' and
|
||||
prefs.double_click)
|
||||
cutter: bpy.props.StringProperty()
|
||||
extend: bpy.props.BoolProperty()
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.extend = event.shift
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
modifier = context.object.modifiers.active
|
||||
if modifier and modifier.type == "BOOLEAN":
|
||||
cutter = modifier.object
|
||||
cutter = bpy.data.objects.get(self.cutter)
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
if not self.extend:
|
||||
for obj in context.scene.objects:
|
||||
obj.select_set(False)
|
||||
|
||||
if cutter:
|
||||
cutter.select_set(True)
|
||||
context.view_layer.objects.active = cutter
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -93,31 +98,16 @@ class OBJECT_OT_boolean_select_cutter(bpy.types.Operator):
|
||||
|
||||
addon_keymaps = []
|
||||
|
||||
classes = [
|
||||
classes = (
|
||||
OBJECT_OT_select_cutter_canvas,
|
||||
OBJECT_OT_boolean_select_all,
|
||||
OBJECT_OT_boolean_select_cutter,
|
||||
]
|
||||
|
||||
)
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
# KEYMAP
|
||||
addon = bpy.context.window_manager.keyconfigs.addon
|
||||
km = addon.keymaps.new(name="Property Editor", space_type='PROPERTIES')
|
||||
|
||||
kmi = km.keymap_items.new("object.boolean_select_cutter", type='LEFTMOUSE', value='DOUBLE_CLICK')
|
||||
kmi.active = True
|
||||
addon_keymaps.append((km, kmi))
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
# KEYMAP
|
||||
for km, kmi in addon_keymaps:
|
||||
km.keymap_items.remove(kmi)
|
||||
addon_keymaps.clear()
|
||||
|
||||
Reference in New Issue
Block a user