Skip to content

Commit

Permalink
feat: add duplicate utility operator and corresponding fast menu inte…
Browse files Browse the repository at this point in the history
…gration
  • Loading branch information
tristan-hm authored Jul 28, 2024
1 parent 423de48 commit 61db54a
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 5 deletions.
2 changes: 2 additions & 0 deletions booleans/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from . import hydrate
from . import swap_solver
from . import boolean_inset
from . import duplicate_utility


registerables = (
Expand All @@ -39,6 +40,7 @@
hydrate,
swap_solver,
boolean_inset,
duplicate_utility,
)


Expand Down
119 changes: 119 additions & 0 deletions booleans/duplicate_utility.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# ███╗ ██╗██████╗
# ████╗ ██║██╔══██╗
# ██╔██╗ ██║██║ ██║
# ██║╚██╗██║██║ ██║
# ██║ ╚████║██████╔╝
# ╚═╝ ╚═══╝╚═════╝
#
# ND (Non-Destructive) Blender Add-on
# Copyright (C) 2024 Tristan S. & Ian J. (HugeMenace)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# ---
# Contributors: Tristo (HM)
# ---

import bpy
import inspect
from .. lib.collections import hide_utils_collection, get_utils_layer, isolate_in_utils_collection
from .. lib.objects import get_all_util_objects, get_real_active_object
from .. lib.polling import ctx_obj_mode, ctx_objects_selected
from .. lib.modifiers import move_mod_to_index


class ND_OT_duplicate_utility(bpy.types.Operator):
bl_idname = "nd.duplicate_utility"
bl_label = "Duplicate Utility"
bl_description = """Duplicate the selected utility object and the associated modifier(s)
SHIFT — Do not duplicate intersect target objects
ALT — Create a linked duplicate"""


@classmethod
def poll(cls, context):
target_object = get_real_active_object(context)
return ctx_obj_mode(context) and ctx_objects_selected(context, 1)


def invoke(self, context, event):
self.ignore_intersects = event.shift
self.linked_duplicate = event.alt

old_utility_object = context.active_object

if self.linked_duplicate:
bpy.ops.object.duplicate_move_linked()
else:
bpy.ops.object.duplicate()

new_utility_object = context.active_object

targets = []
all_scene_objects = [obj for obj in bpy.context.view_layer.objects if obj.type == 'MESH']

# Get all objects with boolean modifiers that reference the utility object
for obj in all_scene_objects:
if obj == old_utility_object:
continue

obj_mods = list(obj.modifiers)
applicable_mods = []
for index, mod in enumerate(obj_mods):
if mod.type == 'BOOLEAN' and mod.object == old_utility_object:
applicable_mods.append((mod, index))

if len(applicable_mods) > 0:
targets.append((obj, applicable_mods))

for obj, applicable_mods in targets:
for old_mod, old_mod_index in applicable_mods:
# If the intersect target is not being ignored, duplicate the intersect target object
# and set it as the new object for the boolean modifier.
if not self.ignore_intersects and old_mod.operation == 'INTERSECT':
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
context.view_layer.objects.active = obj
bpy.ops.object.duplicate()
obj = context.active_object

# Create a new boolean modifier with the same properties as the old one.
new_mod = obj.modifiers.new(old_mod.name, 'BOOLEAN')
old_mod_props = inspect.getmembers(old_mod, lambda a: not(inspect.isroutine(a)))
for prop in old_mod_props:
try:
setattr(new_mod, prop[0], prop[1])
except:
pass

new_mod.object = new_utility_object
move_mod_to_index(obj, new_mod.name, old_mod_index+1)

if not self.ignore_intersects and old_mod.operation == 'INTERSECT':
bpy.ops.object.modifier_remove(modifier=old_mod.name)

bpy.ops.object.select_all(action='DESELECT')
new_utility_object.select_set(True)
context.view_layer.objects.active = new_utility_object
bpy.ops.transform.translate('INVOKE_DEFAULT')

return {'FINISHED'}


def register():
bpy.utils.register_class(ND_OT_duplicate_utility)


def unregister():
bpy.utils.unregister_class(ND_OT_duplicate_utility)
1 change: 1 addition & 0 deletions interface/fast_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ def draw_single_object_mesh_predictions(self, context, layout):
layout.separator()
layout.operator("nd.hydrate", icon=icons['nd.hydrate'])
layout.operator("nd.swap_solver", text="Swap Solver (Booleans)", icon=icons['nd.swap_solver'])
layout.operator("nd.duplicate_utility", icon=icons['nd.duplicate_utility'])

return SECTION_COUNT

Expand Down
1 change: 1 addition & 0 deletions interface/ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
None, # Separator
("nd.hydrate", 'SHADING_RENDERED', None, None, False),
("nd.swap_solver", 'CON_OBJECTSOLVER', None, None, False),
("nd.duplicate_utility", 'CON_SIZELIKE', None, None, True),
]

bevel_ops = [
Expand Down
14 changes: 9 additions & 5 deletions lib/modifiers.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ def new_modifier(object, mod_name, mod_type, rectify=True):
return mod


def move_mod_to_index(object, mod_name, index):
if app_minor_version() < (4, 0):
bpy.ops.object.modifier_move_to_index({'object': object}, modifier=mod_name, index=index)
else:
with bpy.context.temp_override(object=object):
bpy.ops.object.modifier_move_to_index(modifier=mod_name, index=index)


def rectify_mod_order(object, mod_name):
mods = list(object.modifiers)

Expand Down Expand Up @@ -81,11 +89,7 @@ def rectify_mod_order(object, mod_name):
if matching_mod_index is None:
return

if app_minor_version() < (4, 0):
bpy.ops.object.modifier_move_to_index({'object': object}, modifier=mod_name, index=matching_mod_index)
else:
with bpy.context.temp_override(object=object):
bpy.ops.object.modifier_move_to_index(modifier=mod_name, index=matching_mod_index)
move_mod_to_index(object, mod_name, matching_mod_index)


def get_sba_mod(object):
Expand Down

0 comments on commit 61db54a

Please sign in to comment.