252 lines
8.3 KiB
Python
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
|