2025-07-01
This commit is contained in:
@@ -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 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(
|
||||
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()
|
||||
Reference in New Issue
Block a user