update addons (for 5.2)

This commit is contained in:
Nathan
2026-07-14 14:44:58 -06:00
parent a232ea1c9c
commit e3310263f3
246 changed files with 21097 additions and 9607 deletions
@@ -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()