2025-12-01

This commit is contained in:
2026-03-17 14:58:51 -06:00
parent 183e865f8b
commit 4b82b57113
6846 changed files with 954887 additions and 162606 deletions
@@ -1,10 +1,19 @@
import bpy
from . import (
boolean,
canvas,
cutter,
select,
)
if "bpy" in locals():
import importlib
for mod in [boolean,
canvas,
cutter,
select,
]:
importlib.reload(mod)
else:
import bpy
from . import (
boolean,
canvas,
cutter,
select,
)
#### ------------------------------ REGISTRATION ------------------------------ ####
@@ -1,35 +1,39 @@
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.modifier import (
add_boolean_modifier,
apply_modifiers,
)
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,
)
#### ------------------------------ PROPERTIES ------------------------------ ####
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.")),
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(
@@ -60,7 +64,7 @@ class ModifierProperties():
layout.prop(self, "material_mode")
layout.prop(self, "use_self")
layout.prop(self, "use_hole_tolerant")
elif prefs.solver == 'FAST':
elif prefs.solver == 'FLOAT':
layout.prop(self, "double_threshold")
@@ -68,20 +72,20 @@ class ModifierProperties():
#### ------------------------------ /brush_boolean/ ------------------------------ ####
class BrushBoolean(ModifierProperties):
@classmethod
def poll(cls, context):
return basic_poll(cls, context)
def invoke(self, context, event):
# abort_when_no_selected_objects
# Abort if there are less than 2 selected objects.
if len(context.selected_objects) < 2:
self.report({'WARNING'}, "Boolean operator needs at least two selected objects")
return {'CANCELLED'}
# abort_when_linked
# Abort if active object is 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:
self.report({'WARNING'}, "Boolean operators cannot be performed on linked objects")
return {'CANCELLED'}
return self.execute(context)
@@ -90,20 +94,23 @@ class BrushBoolean(ModifierProperties):
def execute(self, context):
prefs = context.preferences.addons[base_package].preferences
canvas = context.active_object
cutters = list_candidate_objects(self, context, context.active_object)
# Create Slices
if len(cutters) == 0:
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"""
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)
add_boolean_modifier(self, context, slice, cutter, "INTERSECT", prefs.solver, pin=prefs.pin)
for cutter in self.cutters:
for cutter in cutters:
mode = "DIFFERENCE" if self.mode == "SLICE" else self.mode
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)
add_boolean_modifier(self, context, canvas, cutter, mode, prefs.solver, pin=prefs.pin)
context.view_layer.objects.active = canvas
canvas.booleans.canvas = True
return {'FINISHED'}
@@ -115,10 +122,6 @@ class OBJECT_OT_boolean_brush_union(bpy.types.Operator, BrushBoolean):
bl_description = "Merge selected objects into active one"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return basic_poll(context)
mode = "UNION"
@@ -128,10 +131,6 @@ class OBJECT_OT_boolean_brush_intersect(bpy.types.Operator, BrushBoolean):
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"
@@ -141,10 +140,6 @@ class OBJECT_OT_boolean_brush_difference(bpy.types.Operator, BrushBoolean):
bl_description = "Subtract selected objects from active one"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return basic_poll(context)
mode = "DIFFERENCE"
@@ -154,10 +149,6 @@ class OBJECT_OT_boolean_brush_slice(bpy.types.Operator, BrushBoolean):
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"
@@ -165,90 +156,89 @@ class OBJECT_OT_boolean_brush_slice(bpy.types.Operator, BrushBoolean):
#### ------------------------------ /auto_boolean/ ------------------------------ ####
class AutoBoolean(ModifierProperties):
@classmethod
def poll(cls, context):
return basic_poll(cls, context)
def invoke(self, context, event):
# abort_when_no_selected_objects
# Abort if there are less than 2 selected objects.
if len(context.selected_objects) < 2:
self.report({'WARNING'}, "Boolean operator needs at least two selected objects")
return {'CANCELLED'}
# abort_when_linked
# Abort if active object is 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:
self.report({'ERROR'}, "Modifiers cannot be applied to linked object")
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)
return destructive_op_confirmation(self, context, event, [context.active_object], title="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)
# 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'}
if len(cutters) == 0:
return {'CANCELLED'}
# Create Slices
# 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"""
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)
add_boolean_modifier(self, context, slice, cutter, "INTERSECT", prefs.solver, apply=True, single_user=True)
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 self.cutters:
# Add Modifier (& Apply)
for cutter in cutters:
# Add boolean modifier on canvas.
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)
modifier = add_boolean_modifier(self, context, canvas, cutter, mode, prefs.solver, pin=prefs.pin)
new_modifiers[canvas].append(modifier)
# Transfer Children
# Transfer cutters children to canvas.
for child in cutter.children:
change_parent(child, canvas)
# Delete Cutter
# 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.
for obj, modifiers in new_modifiers.items():
modifiers = self._get_modifiers_to_apply(prefs, obj, modifiers)
apply_modifiers(context, obj, modifiers)
# Delete cutters.
for cutter in cutters:
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'}
def _get_modifiers_to_apply(self, prefs, obj, new_modifiers) -> list:
"""Returns a list of modifiers that need to be applied based on add-on preferences."""
if prefs.apply_order == 'ALL':
modifiers = [mod for mod in obj.modifiers]
elif prefs.apply_order == 'BOOLEANS':
modifiers = new_modifiers
elif prefs.apply_order == 'BEFORE':
modifiers = list_pre_boolean_modifiers(obj)
return modifiers
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"
@@ -258,10 +248,6 @@ class OBJECT_OT_boolean_auto_difference(bpy.types.Operator, AutoBoolean):
bl_description = "Subtract selected objects from active one"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return basic_poll(context)
mode = "DIFFERENCE"
@@ -271,10 +257,6 @@ class OBJECT_OT_boolean_auto_intersect(bpy.types.Operator, AutoBoolean):
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"
@@ -284,10 +266,6 @@ class OBJECT_OT_boolean_auto_slice(bpy.types.Operator, AutoBoolean):
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"
@@ -317,7 +295,7 @@ def register():
addon = bpy.context.window_manager.keyconfigs.addon
km = addon.keymaps.new(name="Object Mode")
# brush_operators
# Brush Operators
kmi = km.keymap_items.new("object.boolean_brush_union", 'NUMPAD_PLUS', 'PRESS', ctrl=True)
kmi.active = True
addon_keymaps.append((km, kmi))
@@ -334,7 +312,7 @@ def register():
kmi.active = True
addon_keymaps.append((km, kmi))
# auto_operators
# 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))
@@ -1,14 +1,17 @@
import bpy, itertools
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.modifier import (
apply_modifiers,
)
from ..functions.object import (
apply_modifier,
convert_to_mesh,
object_visibility_set,
delete_empty_collection,
delete_cutter,
@@ -36,7 +39,7 @@ class OBJECT_OT_boolean_toggle_all(bpy.types.Operator):
@classmethod
def poll(cls, context):
return basic_poll(context, check_linked=True) and is_canvas(context.active_object)
return basic_poll(cls, context, check_linked=True) and is_canvas(context.active_object)
def execute(self, context):
canvases = list_selected_canvases(context)
@@ -79,7 +82,7 @@ class OBJECT_OT_boolean_remove_all(bpy.types.Operator):
@classmethod
def poll(cls, context):
return basic_poll(context, check_linked=True) and is_canvas(context.active_object)
return basic_poll(cls, context, check_linked=True) and is_canvas(context.active_object)
def execute(self, context):
prefs = context.preferences.addons[base_package].preferences
@@ -153,29 +156,12 @@ class OBJECT_OT_boolean_apply_all(bpy.types.Operator):
@classmethod
def poll(cls, context):
return basic_poll(context, check_linked=True) and is_canvas(context.active_object)
return basic_poll(cls, 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)
self.canvases = list_selected_canvases(context)
return destructive_op_confirmation(self, context, event, self.canvases, title="Apply Boolean Cutters")
def execute(self, context):
@@ -184,6 +170,8 @@ class OBJECT_OT_boolean_apply_all(bpy.types.Operator):
cutters, __ = list_canvas_cutters(self.canvases)
slices = list_canvas_slices(self.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 face in cutter.data.polygons:
face.select = True
@@ -193,17 +181,13 @@ class OBJECT_OT_boolean_apply_all(bpy.types.Operator):
# Apply Modifiers
if prefs.apply_order == 'ALL':
convert_to_mesh(context, canvas)
modifiers = [mod for mod in canvas.modifiers]
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)
modifiers = [mod for mod in canvas.modifiers if mod.type == 'BOOLEAN' and "boolean_" in mod.name]
apply_modifiers(context, canvas, modifiers)
# remove_boolean_properties
canvas.booleans.canvas = False
@@ -4,9 +4,12 @@ from .. import __package__ as base_package
from ..functions.poll import (
basic_poll,
is_instanced_data,
destructive_op_confirmation,
)
from ..functions.modifier import (
apply_modifiers,
)
from ..functions.object import (
apply_modifier,
object_visibility_set,
delete_empty_collection,
delete_cutter,
@@ -45,7 +48,7 @@ class OBJECT_OT_boolean_toggle_cutter(bpy.types.Operator):
@classmethod
def poll(cls, context):
return basic_poll(context, check_linked=True)
return basic_poll(cls, context, check_linked=True)
def execute(self, context):
if self.method == 'SPECIFIED':
@@ -118,7 +121,7 @@ class OBJECT_OT_boolean_remove_cutter(bpy.types.Operator):
@classmethod
def poll(cls, context):
return basic_poll(context, check_linked=True)
return basic_poll(cls, context, check_linked=True)
def execute(self, context):
prefs = context.preferences.addons[base_package].preferences
@@ -220,7 +223,7 @@ class OBJECT_OT_boolean_apply_cutter(bpy.types.Operator):
@classmethod
def poll(cls, context):
return basic_poll(context, check_linked=True)
return basic_poll(cls, context, check_linked=True)
def invoke(self, context, event):
@@ -232,25 +235,9 @@ class OBJECT_OT_boolean_apply_cutter(bpy.types.Operator):
elif self.method == 'ALL':
self.cutters = list_selected_cutters(context)
self.canvases = []
self.canvases = list_cutter_users(self.cutters)
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)
return destructive_op_confirmation(self, context, event, self.canvases, title="Apply Boolean Cutter")
def execute(self, context):
@@ -258,6 +245,8 @@ class OBJECT_OT_boolean_apply_cutter(bpy.types.Operator):
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
@@ -266,10 +255,12 @@ class OBJECT_OT_boolean_apply_cutter(bpy.types.Operator):
for canvas in self.canvases:
context.view_layer.objects.active = canvas
boolean_mods = []
for mod in canvas.modifiers:
if "boolean_" in mod.name:
if mod.object in self.cutters:
apply_modifier(context, canvas, mod, single_user=True)
boolean_mods.append(mod)
apply_modifiers(context, canvas, boolean_mods)
# remove_canvas_property_if_needed
other_cutters, __ = list_canvas_cutters([canvas])
@@ -281,9 +272,11 @@ class OBJECT_OT_boolean_apply_cutter(bpy.types.Operator):
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:
apply_modifier(context, slice, mod, single_user=True)
boolean_mods.append(mod)
apply_modifiers(context, slice, boolean_mods)
unused_cutters, leftovers = list_unused_cutters(self.cutters, self.canvases, do_leftovers=True)
@@ -25,7 +25,7 @@ class OBJECT_OT_select_cutter_canvas(bpy.types.Operator):
@classmethod
def poll(cls, context):
return basic_poll(context) and context.active_object.booleans.cutter
return basic_poll(cls, context) and context.active_object.booleans.cutter
def execute(self, context):
cutters = list_selected_cutters(context)
@@ -48,7 +48,7 @@ class OBJECT_OT_boolean_select_all(bpy.types.Operator):
@classmethod
def poll(cls, context):
return basic_poll(context) and is_canvas(context.active_object)
return basic_poll(cls, context) and is_canvas(context.active_object)
def execute(self, context):
canvases = list_selected_canvases(context)
@@ -72,7 +72,7 @@ class OBJECT_OT_boolean_select_cutter(bpy.types.Operator):
@classmethod
def poll(cls, context):
prefs = context.preferences.addons[base_package].preferences
return (basic_poll(context) and active_modifier_poll(context) and
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)