2025-07-01

This commit is contained in:
2026-03-17 14:30:01 -06:00
parent f9a22056dd
commit 62b5978595
4579 changed files with 1257472 additions and 0 deletions
@@ -0,0 +1,25 @@
import bpy
from . import (
boolean,
canvas,
cutter,
select,
)
#### ------------------------------ REGISTRATION ------------------------------ ####
modules = [
boolean,
canvas,
cutter,
select,
]
def register():
for module in modules:
module.register()
def unregister():
for module in reversed(modules):
module.unregister()
@@ -0,0 +1,362 @@
import bpy
from .. import __package__ as base_package
from ..functions.poll import (
basic_poll,
is_linked,
is_instanced_data,
)
from ..functions.object import (
apply_modifier,
convert_to_mesh,
add_boolean_modifier,
set_cutter_properties,
change_parent,
create_slice,
delete_cutter,
)
from ..functions.list import (
list_candidate_objects,
list_cutter_users,
list_pre_boolean_modifiers,
)
class ModifierProperties():
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 doesnt exist on the\n"
"modifier object, the face will use the same material slot or the first if the object doesnt 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(
name = "Self Intersection",
description = "Allow self-intersection in operands",
default = False,
)
use_hole_tolerant: bpy.props.BoolProperty(
name = "Hole Tolerant",
description = "Better results when there are holes (slower)",
default = False,
)
double_threshold: bpy.props.FloatProperty(
name = "Overlap Threshold",
description = "Threshold for checking overlapping geometry",
subtype = 'DISTANCE',
min = 0, max = 1, precision = 12, step = 0.0001,
default = 0.000001,
)
def draw(self, context):
prefs = context.preferences.addons[base_package].preferences
layout = self.layout
layout.use_property_split = True
if prefs.solver == 'EXACT':
layout.prop(self, "material_mode")
layout.prop(self, "use_self")
layout.prop(self, "use_hole_tolerant")
elif prefs.solver == 'FAST':
layout.prop(self, "double_threshold")
#### ------------------------------ /brush_boolean/ ------------------------------ ####
class BrushBoolean(ModifierProperties):
def invoke(self, context, event):
# abort_when_no_selected_objects
if len(context.selected_objects) < 2:
self.report({'WARNING'}, "Boolean operator needs at least two selected objects")
return {'CANCELLED'}
# abort_when_linked
if is_linked(context, context.active_object):
self.report({'WARNING'}, "Booleans can not be performed on linked objects")
return {'CANCELLED'}
self.cutters = list_candidate_objects(self, context, context.active_object)
if len(self.cutters) == 0:
return {'CANCELLED'}
return self.execute(context)
def execute(self, context):
prefs = context.preferences.addons[base_package].preferences
canvas = context.active_object
# Create Slices
if self.mode == "SLICE":
for cutter in self.cutters:
"""NOTE: Slices need to be created in separate loop to avoid inheriting boolean modifiers that operator adds"""
slice = create_slice(context, canvas, modifier=True)
add_boolean_modifier(self, context, slice, cutter, "INTERSECT", prefs.solver)
for cutter in self.cutters:
set_cutter_properties(context, canvas, cutter, self.mode, parent=prefs.parent, collection=prefs.use_collection)
add_boolean_modifier(self, context, canvas, cutter, "DIFFERENCE" if self.mode == "SLICE" else self.mode, prefs.solver, pin=prefs.pin)
context.view_layer.objects.active = canvas
canvas.booleans.canvas = True
return {'FINISHED'}
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'}
@classmethod
def poll(cls, context):
return basic_poll(context)
mode = "UNION"
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'}
@classmethod
def poll(cls, context):
return basic_poll(context)
mode = "INTERSECT"
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'}
@classmethod
def poll(cls, context):
return basic_poll(context)
mode = "DIFFERENCE"
class OBJECT_OT_boolean_brush_slice(bpy.types.Operator, BrushBoolean):
bl_idname = "object.boolean_brush_slice"
bl_label = "Boolean Slice (Brush)"
bl_description = "Slice active object along the selected ones. Will create slices as separate objects"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return basic_poll(context)
mode = "SLICE"
#### ------------------------------ /auto_boolean/ ------------------------------ ####
class AutoBoolean(ModifierProperties):
def invoke(self, context, event):
# abort_when_no_selected_objects
if len(context.selected_objects) < 2:
self.report({'WARNING'}, "Boolean operator needs at least two selected objects")
return {'CANCELLED'}
# abort_when_linked
if is_linked(context, context.active_object):
self.report({'ERROR'}, "Modifiers can't be applied to linked object")
return {'CANCELLED'}
self.cutters = list_candidate_objects(self, context, context.active_object)
if len(self.cutters) == 0:
return {'CANCELLED'}
if is_instanced_data(context.active_object):
return context.window_manager.invoke_confirm(self, event,
title="Auto Boolean", confirm_text="Yes", icon='WARNING',
message=("Canvas object has instanced object data.\n"
"In order to apply modifiers, it needs to be made single-user.\n"
"Do you proceed?"))
else:
return self.execute(context)
def execute(self, context):
prefs = context.preferences.addons[base_package].preferences
canvas = context.active_object
# apply_modifiers
if (prefs.apply_order == 'ALL') or (prefs.apply_order == 'BEFORE' and prefs.pin == False):
convert_to_mesh(context, canvas)
else:
if canvas.data.shape_keys:
self.report({'ERROR'}, "Modifiers can't be applied to object with shape keys")
return {'CANCELLED'}
# Create Slices
if self.mode == "SLICE":
for cutter in self.cutters:
"""NOTE: Slices need to be created in separate loop to avoid inheriting boolean modifiers that operator adds"""
slice = create_slice(context, canvas)
add_boolean_modifier(self, context, slice, cutter, "INTERSECT", prefs.solver, apply=True, single_user=True)
for cutter in self.cutters:
# Add Modifier (& Apply)
mode = "DIFFERENCE" if self.mode == "SLICE" else self.mode
add_boolean_modifier(self, context, canvas, cutter, mode, prefs.solver, apply=True, pin=prefs.pin, single_user=True)
# Transfer Children
for child in cutter.children:
change_parent(child, canvas)
# Delete Cutter
delete_cutter(cutter)
if self.mode == "SLICE":
slice.select_set(True)
context.view_layer.objects.active = slice
# apply_modifiers_before_final_boolean
if prefs.apply_order == 'BEFORE' and prefs.pin:
modifiers = list_pre_boolean_modifiers(canvas)
for mod in modifiers:
apply_modifier(context, canvas, mod, single_user=True)
return {'FINISHED'}
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'}
@classmethod
def poll(cls, context):
return basic_poll(context)
mode = "UNION"
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'}
@classmethod
def poll(cls, context):
return basic_poll(context)
mode = "DIFFERENCE"
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'}
@classmethod
def poll(cls, context):
return basic_poll(context)
mode = "INTERSECT"
class OBJECT_OT_boolean_auto_slice(bpy.types.Operator, AutoBoolean):
bl_idname = "object.boolean_auto_slice"
bl_label = "Boolean Slice (Auto)"
bl_description = "Slice active object along the selected ones. Will create slices as separate objects"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return basic_poll(context)
mode = "SLICE"
#### ------------------------------ REGISTRATION ------------------------------ ####
addon_keymaps = []
classes = [
OBJECT_OT_boolean_brush_union,
OBJECT_OT_boolean_brush_difference,
OBJECT_OT_boolean_brush_intersect,
OBJECT_OT_boolean_brush_slice,
OBJECT_OT_boolean_auto_union,
OBJECT_OT_boolean_auto_difference,
OBJECT_OT_boolean_auto_intersect,
OBJECT_OT_boolean_auto_slice,
]
def register():
for cls in classes:
bpy.utils.register_class(cls)
# KEYMAP
addon = bpy.context.window_manager.keyconfigs.addon
km = addon.keymaps.new(name="Object Mode")
# brush_operators
kmi = km.keymap_items.new("object.boolean_brush_union", 'NUMPAD_PLUS', 'PRESS', ctrl=True)
kmi.active = True
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("object.boolean_brush_difference", 'NUMPAD_MINUS', 'PRESS', ctrl=True)
kmi.active = True
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("object.boolean_brush_intersect", 'NUMPAD_ASTERIX', 'PRESS', ctrl=True)
kmi.active = True
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("object.boolean_brush_slice", 'NUMPAD_SLASH', 'PRESS', ctrl=True)
kmi.active = True
addon_keymaps.append((km, kmi))
# auto_operators
kmi = km.keymap_items.new("object.boolean_auto_union", 'NUMPAD_PLUS', 'PRESS', ctrl=True, shift=True)
kmi.active = True
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("object.boolean_auto_difference", 'NUMPAD_MINUS', 'PRESS', ctrl=True, shift=True)
kmi.active = True
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("object.boolean_auto_intersect", 'NUMPAD_ASTERIX', 'PRESS', ctrl=True, shift=True)
kmi.active = True
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("object.boolean_auto_slice", 'NUMPAD_SLASH', 'PRESS', ctrl=True, shift=True)
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()
@@ -0,0 +1,274 @@
import bpy, itertools
from .. import __package__ as base_package
from ..functions.poll import (
basic_poll,
is_canvas,
is_instanced_data,
)
from ..functions.object import (
apply_modifier,
convert_to_mesh,
object_visibility_set,
delete_empty_collection,
delete_cutter,
change_parent,
)
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,
)
#### ------------------------------ OPERATORS ------------------------------ ####
# Toggle All Cutters
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_options = {'UNDO'}
@classmethod
def poll(cls, context):
return basic_poll(context, check_linked=True) and is_canvas(context.active_object)
def execute(self, context):
canvases = list_selected_canvases(context)
cutters, modifiers = list_canvas_cutters(canvases)
slices = list_canvas_slices(canvases)
# Toggle Modifiers
for mod in modifiers:
mod.show_viewport = not mod.show_viewport
mod.show_render = not mod.show_render
# Hide Slices
for slice in slices:
slice.hide_viewport = not slice.hide_viewport
slice.hide_render = not slice.hide_render
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
# 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
return {'FINISHED'}
# Remove All Cutters
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'}
@classmethod
def poll(cls, context):
return basic_poll(context, check_linked=True) and is_canvas(context.active_object)
def execute(self, context):
prefs = context.preferences.addons[base_package].preferences
canvases = list_selected_canvases(context)
cutters, __ = list_canvas_cutters(canvases)
slices = list_canvas_slices(canvases)
# Remove Slices
for slice in slices:
if slice in canvases:
canvases.remove(slice)
delete_cutter(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_boolean_properties
if canvas.booleans.canvas == True:
canvas.booleans.canvas = False
# 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])
return {'FINISHED'}
# Apply All Cutters
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'}
@classmethod
def poll(cls, context):
return basic_poll(context, check_linked=True) and is_canvas(context.active_object)
def invoke(self, context, event):
# Filter Objects
self.canvases = []
for obj in list_selected_canvases(context):
# excude_canvases_with_shape_keys
if obj.data.shape_keys:
self.report({'ERROR'}, f"Modifiers can't be applied to {obj.name} because it has shape keys")
continue
self.canvases.append(obj)
if any(obj for obj in self.canvases if is_instanced_data(obj)):
return context.window_manager.invoke_confirm(self, event,
title="Apply Boolean Cutters", confirm_text="Yes", icon='WARNING',
message=("Canvas object(s) have instanced object data.\n"
"In order to apply modifiers, they need to be made single-user.\n"
"Do you proceed?"))
else:
return self.execute(context)
def execute(self, context):
prefs = context.preferences.addons[base_package].preferences
cutters, __ = list_canvas_cutters(self.canvases)
slices = list_canvas_slices(self.canvases)
for cutter in cutters:
for face in cutter.data.polygons:
face.select = True
for canvas in itertools.chain(self.canvases, slices):
context.view_layer.objects.active = canvas
# Apply Modifiers
if prefs.apply_order == 'ALL':
convert_to_mesh(context, canvas)
elif prefs.apply_order == 'BEFORE':
modifiers = list_pre_boolean_modifiers(canvas)
for mod in modifiers:
apply_modifier(context, canvas, mod, single_user=True)
elif prefs.apply_order == 'BOOLEANS':
for mod in canvas.modifiers:
if mod.type == 'BOOLEAN' and "boolean_" in mod.name:
apply_modifier(context, canvas, mod, single_user=True)
# remove_boolean_properties
canvas.booleans.canvas = False
canvas.booleans.slice = False
# 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])
return {'FINISHED'}
#### ------------------------------ REGISTRATION ------------------------------ ####
addon_keymaps = []
classes = [
OBJECT_OT_boolean_toggle_all,
OBJECT_OT_boolean_remove_all,
OBJECT_OT_boolean_apply_all,
]
def register():
for cls in classes:
bpy.utils.register_class(cls)
# KEYMAP
addon = bpy.context.window_manager.keyconfigs.addon
km = addon.keymaps.new(name="Object Mode")
kmi = km.keymap_items.new("object.boolean_apply_all", 'NUMPAD_ENTER', 'PRESS', shift=True, ctrl=True)
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()
@@ -0,0 +1,351 @@
import bpy
from .. import __package__ as base_package
from ..functions.poll import (
basic_poll,
is_instanced_data,
)
from ..functions.object import (
apply_modifier,
object_visibility_set,
delete_empty_collection,
delete_cutter,
change_parent,
)
from ..functions.list import (
list_canvases,
list_selected_cutters,
list_canvas_cutters,
list_canvas_slices,
list_cutter_users,
list_unused_cutters,
)
#### ------------------------------ OPERATORS ------------------------------ ####
# Toggle Boolean Cutter
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'}
method: bpy.props.EnumProperty(
name = "Method",
items = (('ALL', "All", "Remove cutter from all canvases that use it"),
('SPECIFIED', "Specified", "Remove cutter from specified canvas")),
default = 'ALL',
)
specified_cutter: bpy.props.StringProperty(
)
specified_canvas: bpy.props.StringProperty(
)
@classmethod
def poll(cls, context):
return basic_poll(context, check_linked=True)
def execute(self, context):
if self.method == 'SPECIFIED':
canvases = [context.scene.objects[self.specified_canvas]]
cutters = [context.scene.objects[self.specified_cutter]]
slices = list_canvas_slices(canvases)
elif self.method == 'ALL':
cutters = list_selected_cutters(context)
canvases = list_cutter_users(cutters)
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:
self.report({'INFO'}, "Boolean cutters are not selected")
return {'FINISHED'}
# Remove Boolean Cutter
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'}
method: bpy.props.EnumProperty(
name = "Method",
items = (('ALL', "All", "Remove cutter from all canvases that use it"),
('SPECIFIED', "Specified", "Remove cutter from specified canvas")),
default = 'ALL',
)
specified_cutter: bpy.props.StringProperty(
)
specified_canvas: bpy.props.StringProperty(
)
@classmethod
def poll(cls, context):
return basic_poll(context, check_linked=True)
def execute(self, context):
prefs = context.preferences.addons[base_package].preferences
leftovers = []
if self.method == 'SPECIFIED':
canvases = [context.scene.objects[self.specified_canvas]]
cutters = [context.scene.objects[self.specified_cutter]]
slices = list_canvas_slices(canvases)
elif self.method == 'ALL':
cutters = list_selected_cutters(context)
canvases = 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:
self.report({'INFO'}, "Boolean cutters are not selected")
return {'FINISHED'}
# Apply Boolean Cutter
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'}
method: bpy.props.EnumProperty(
name = "Method",
items = (('ALL', "All", "Remove cutter from all canvases that use it"),
('SPECIFIED', "Specified", "Remove cutter from specified canvas")),
default = 'ALL',
)
specified_cutter: bpy.props.StringProperty(
)
specified_canvas: bpy.props.StringProperty(
)
@classmethod
def poll(cls, context):
return basic_poll(context, check_linked=True)
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)
elif self.method == 'ALL':
self.cutters = list_selected_cutters(context)
self.canvases = []
for obj in list_cutter_users(self.cutters):
# excude_canvases_with_shape_keys
if obj.data.shape_keys:
self.report({'ERROR'}, f"Modifiers can't be applied to {obj.name} because it has shape keys")
continue
self.canvases.append(obj)
if any(obj for obj in self.canvases if is_instanced_data(obj)):
return context.window_manager.invoke_confirm(self, event,
title="Apply Boolean Cutter", confirm_text="Yes", icon='WARNING',
message=("Canvas object(s) have instanced object data.\n"
"In order to apply modifiers, they need to be made single-user.\n"
"Do you proceed?"))
else:
return self.execute(context)
def execute(self, context):
prefs = bpy.context.preferences.addons[base_package].preferences
leftovers = []
if self.cutters:
for cutter in self.cutters:
for face in cutter.data.polygons:
face.select = True
# Apply Modifiers
for canvas in self.canvases:
context.view_layer.objects.active = canvas
for mod in canvas.modifiers:
if "boolean_" in mod.name:
if mod.object in self.cutters:
apply_modifier(context, canvas, mod, single_user=True)
# remove_canvas_property_if_needed
other_cutters, __ = list_canvas_cutters([canvas])
if len(other_cutters) == 0:
canvas.booleans.canvas = False
canvas.booleans.slice = False
if self.method == 'SPECIFIED':
# Apply Modifier for Slices (for_specified_method)
for slice in self.slices:
for mod in slice.modifiers:
if mod.type == 'BOOLEAN' and mod.object in self.cutters:
apply_modifier(context, slice, mod, single_user=True)
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")
return {'FINISHED'}
#### ------------------------------ REGISTRATION ------------------------------ ####
addon_keymaps = []
classes = [
OBJECT_OT_boolean_toggle_cutter,
OBJECT_OT_boolean_remove_cutter,
OBJECT_OT_boolean_apply_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="Object Mode")
kmi = km.keymap_items.new("object.boolean_apply_cutter", 'NUMPAD_ENTER', 'PRESS', ctrl=True)
kmi.properties.method = 'ALL'
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()
@@ -0,0 +1,123 @@
import bpy
from .. import __package__ as base_package
from ..functions.poll import (
basic_poll,
active_modifier_poll,
is_canvas,
)
from ..functions.list import (
list_selected_cutters,
list_selected_canvases,
list_canvas_cutters,
list_cutter_users,
)
#### ------------------------------ OPERATORS ------------------------------ ####
# Select Cutter Canvas
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_options = {'UNDO'}
@classmethod
def poll(cls, context):
return basic_poll(context) and context.active_object.booleans.cutter
def execute(self, context):
cutters = list_selected_cutters(context)
canvases = list_cutter_users(cutters)
# Select Canvases
bpy.ops.object.select_all(action='DESELECT')
for canvas in canvases:
canvas.select_set(True)
return {'FINISHED'}
# Select All Cutters
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_options = {'UNDO'}
@classmethod
def poll(cls, context):
return basic_poll(context) 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 cutter in cutters:
cutter.select_set(True)
return {'FINISHED'}
# Select Modifier Object
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'}
@classmethod
def poll(cls, context):
prefs = context.preferences.addons[base_package].preferences
return (basic_poll(context) and active_modifier_poll(context) and
context.area.type == 'PROPERTIES' and context.space_data.context == 'MODIFIER' and
prefs.double_click)
def execute(self, context):
modifier = context.object.modifiers.active
if modifier and modifier.type == "BOOLEAN":
cutter = modifier.object
bpy.ops.object.select_all(action='DESELECT')
cutter.select_set(True)
context.view_layer.objects.active = cutter
return {'FINISHED'}
#### ------------------------------ REGISTRATION ------------------------------ ####
addon_keymaps = []
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()