Files
blender-portable-repo/extensions/blender_org/bool_tool/functions/modifier.py
T
2026-07-14 14:44:58 -06:00

252 lines
8.3 KiB
Python

import bpy
import bmesh
from contextlib import contextmanager
from .. import __package__ as base_package
from .mesh import (
is_instanced_mesh,
)
from .object import (
convert_to_mesh,
)
#### ------------------------------ /list/ ------------------------------ ####
def enumerate_boolean_modifiers(obj) -> int:
"""Returns the number of Boolean modifiers on the object."""
num = 0
for mod in obj.modifiers:
if is_boolean_modifier(mod):
num += 1
return num
def get_modifiers_to_apply(context, obj, custom_list=None) -> list:
"""Returns the list of modifiers that need to be applied based on add-on preferences."""
prefs = context.preferences.addons[base_package].preferences
# Apply all modifiers.
if prefs.apply_order == 'ALL':
modifiers = list(obj.modifiers)
# Apply only Boolean modifiers.
elif prefs.apply_order == 'BOOLEANS':
if custom_list is None:
modifiers = [mod for mod in obj.modifiers if is_boolean_modifier(mod)]
else:
modifiers = custom_list
# Apply all modifiers that come before last Boolean modifier.
elif prefs.apply_order == 'BEFORE':
# Find the index of a last Boolean modifier.
last_boolean_index = -1
for i in reversed(range(len(obj.modifiers))):
if obj.modifiers[i].type == 'BOOLEAN':
last_boolean_index = i
break
# If a Boolean modifier is found, list all modifiers that come before it.
if last_boolean_index != -1:
modifiers = [mod for mod in obj.modifiers[:last_boolean_index + 1]]
else:
modifiers = []
return modifiers
#### ------------------------------ /poll/ ------------------------------ ####
def is_boolean_modifier(mod, check_cutter=True) -> bool:
"""Checks if a modifier is a Boolean modifier (and optionally if it has a valid cutter)."""
if mod is None:
return False
if mod.type != 'BOOLEAN':
return False
if check_cutter and mod.object is None:
return False
return True
#### ------------------------------ FUNCTIONS ------------------------------ ####
def add_boolean_modifier(self, context, obj, cutter, mode, solver, pin=False, redo=True):
"""Adds the Boolean modifier with specified cutter and properties to a given object."""
if bpy.app.version < (5, 0, 0) and solver == 'FLOAT':
solver = 'FAST'
prefs = context.preferences.addons[base_package].preferences
name = "boolean_" + cutter.name.replace("boolean_", "")
modifier = obj.modifiers.new(name, 'BOOLEAN')
modifier.operation = mode
modifier.object = cutter
modifier.solver = solver
modifier.show_in_editmode = prefs.show_in_editmode
# Set solver options (inherited from operator properties, i.e. `self`).
if redo:
modifier.material_mode = self.material_mode
modifier.use_self = self.use_self
modifier.use_hole_tolerant = self.use_hole_tolerant
modifier.double_threshold = self.double_threshold
# Move modifier to the index 0 (make it first in the stack).
if pin:
index = obj.modifiers.find(modifier.name)
obj.modifiers.move(index, 0)
return modifier
def apply_modifiers(context, obj, modifiers: list, force_clean=False):
"""
Apply modifiers on object.
Instead of using `bpy.ops.object.modifier_apply`, by default this function uses
`to_mesh` built-in function to create a temporary mesh from the evaluated object
(basically with visible modifiers applied). Temporary mesh is then transferred
to objects mesh using `bmesh`.
This method is up to 2x faster, although it's considered experimental
and may fail in some cases, so a fallback to `bpy.ops.object.modifier_apply` is kept.
"""
prefs = context.preferences.addons[base_package].preferences
_stored_active_obj = context.active_object
# Make object data unique if it's instanced.
if is_instanced_mesh(obj.data):
context.active_object.data = context.active_object.data.copy()
try:
# Don't use this method if it's not enabled by user in preferences, unless caller forces it.
if not prefs.fast_modifier_apply:
if not force_clean:
raise Exception()
context.view_layer.objects.active = obj
with hide_modifiers(obj, excluding=modifiers):
# Create a temporary mesh from evaluated object.
depsgraph = context.evaluated_depsgraph_get()
evaluated_obj = obj.evaluated_get(depsgraph)
temp_data = evaluated_obj.to_mesh(preserve_all_data_layers=True,
depsgraph=depsgraph)
# Create `bmesh` from temporary mesh and update edit mesh.
if context.mode == 'EDIT_MESH':
bm = bmesh.from_edit_mesh(obj.data)
bm.clear()
bm.from_mesh(temp_data)
bmesh.update_edit_mesh(obj.data)
else:
bm = bmesh.new()
bm.from_mesh(temp_data)
bm.to_mesh(obj.data)
bm.free()
evaluated_obj.to_mesh_clear()
# Remove modifiers.
for mod in modifiers:
obj.modifiers.remove(mod)
# Remove shape keys if there are any.
# (after above operations none of the shape keys have any effect).
if obj.data.shape_keys:
obj.shape_key_clear()
except Exception as e:
# print("Error applying modifiers with `bmesh` method:", e, "falling back to `bpy.ops` method")
context_override = {"active_object": obj, "mode": 'OBJECT'}
with context.temp_override(**context_override):
# Apply shape keys if there are any.
if obj.data.shape_keys:
bpy.ops.object.shape_key_remove(all=True, apply_mix=True)
# If all modifiers need to be applied convert to Mesh.
if modifiers == obj.modifiers.values():
print("Applying all modifiers by converting to Mesh")
convert_to_mesh(context, obj)
return
for mod in modifiers:
bpy.ops.object.modifier_apply(modifier=mod.name)
context.view_layer.objects.active = _stored_active_obj
@contextmanager
def hide_modifiers(obj, excluding: list):
"""Hides all modifiers of a given object in the viewport except those in `excluding` list."""
visible_modifiers = []
for mod in obj.modifiers:
if mod in excluding:
continue
if mod.show_viewport == True:
visible_modifiers.append(mod)
mod.show_viewport = False
try:
yield
finally:
for mod in visible_modifiers:
mod.show_viewport = True
def add_modifier_asset(obj, path: str, asset: str):
"""Loads in the node group asset and adds a Geometry Nodes modifier using it."""
try:
# Load in the node group.
if bpy.app.version >= (5, 0, 0):
with bpy.data.libraries.load(path, link=True, pack=True) as (data_from, data_to):
if asset in data_from.node_groups:
data_to.node_groups = [asset]
else:
with bpy.data.libraries.load(path) as (data_from, data_to):
if asset in data_from.node_groups:
data_to.node_groups = [asset]
node_group = bpy.data.node_groups[asset]
# Add modifier to the object.
mod = obj.modifiers.new(asset, type='NODES')
mod.node_group = node_group
mod.show_group_selector = False
mod.show_manage_panel = False
return mod
except Exception as e:
print("Modifier node group could not be loaded:", e)
return None
def update_modifier_input(modifier, socket: str, value):
"""Change the value of the geometry nodes modifier input socket."""
try:
if bpy.app.version >= (5, 2, 0):
socket = getattr(modifier.properties.inputs, socket)
socket.value = value
else:
modifier[f"{socket}"] = value
except:
"""
NOTE: There are plethora of reasons why this can fail, node trees are finicky.
Accounting for all possible cases is borderline impossible, so this check is necessary
to at least fail silently and not throw Python error to users.
"""
pass