Files
blender-portable-repo/scripts/addons/lattice_magic/tweak_lattice.py
T
2026-03-17 14:58:51 -06:00

868 lines
28 KiB
Python

# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
# Inspired by https://twitter.com/soyposmoderno/status/1307222594047758337
# This lets you create an empty hooked up to a Lattice to deform all selected
# objects. A root empty is also created that can be (manually) parented to a
# rig in order to use this for animation.
import bpy
from bpy.props import (
FloatProperty,
IntVectorProperty,
PointerProperty,
StringProperty,
EnumProperty,
)
from bpy.types import (
Operator,
Object,
Lattice,
VertexGroup,
Scene,
Collection,
Modifier,
Panel,
PropertyGroup,
)
from typing import List, Tuple
from mathutils import Matrix, Vector, kdtree
from math import *
from rna_prop_ui import rna_idprop_ui_create
from .utils import clamp, get_lattice_vertex_index, simple_driver, bounding_box_center_of_objects
TWEAKLAT_COLL_NAME = 'Tweak Lattices'
FALLOFF_TYPES = {
# Since these expressions manipulate the shape of the lattice,
# which then manipulates the shape of the mesh,
# it's hard to come up with these functions in any meaningful way.
# So, it was done more so with artistic trial and error.
'LINEAR': lambda x: x + x * 0.1,
'CONSTANT': lambda x: 1,
'SHARP': lambda x: pow(x, 2) * 1.25,
'ROOT': lambda x: pow(x, 0.5) * 1.05,
'SMOOTH': lambda x: (1 - cos(x * pi)) / 2,
'SPHERE': lambda x: sin(pow(x, 0.5)) * 1.25,
'DONUT': lambda x: (sin(pow(x, 0.5)) - pow(x, 2)) * 2.5,
}
class OBJECT_OT_tweaklattice_create(Operator):
"""Create a lattice setup to deform selected objects"""
bl_idname = "lattice.create_tweak_lattice"
bl_label = "Create Tweak Lattice"
bl_options = {'REGISTER', 'UNDO'}
name: StringProperty(
name="Name",
description="A unique name to identify this tweak lattice. Used in helper objects and modifiers",
default="Tweak",
)
resolution: IntVectorProperty(name="Resolution", default=(25, 25, 25), min=6, max=64)
location: EnumProperty(
name="Location",
items=[
('CURSOR', "3D Cursor", "Create at the location and orientation of the 3D cursor."),
('CENTER', "Center", "Create at the bounding box center of all selected objects."),
('PARENT', "Parent", "Create at the location of the parent object or bone."),
],
)
radius: FloatProperty(
name="Radius",
description="Radius of influence of this lattice. Can be changed later",
default=0.1,
min=0.0001,
max=1000,
soft_max=2,
)
parent_method: EnumProperty(
name="Parent Method",
description="How to parent the tweak lattice",
items=[
(
'AUTO',
'Automatic',
"Parent using an Armature constraint which mimics the deformation of the nearest vertex to the cursor",
),
(
'MANUAL',
'Manual',
"Manually select a single object or bone as the tweak lattice parent",
),
],
default='AUTO',
)
parent_bone: StringProperty(name="Bone", description="Bone to use as parent")
@classmethod
def poll(cls, context):
for ob in context.selected_objects:
if ob.type == 'MESH':
return True
cls.poll_message_set("No selected mesh objects.")
return False
def invoke(self, context, _event):
parent_obj = context.object
for m in parent_obj.modifiers:
if m.type == 'ARMATURE' and m.object:
parent_obj = m.object
if self.parent_bone not in parent_obj.data.bones:
self.parent_bone = ""
break
context.scene.tweak_lattice_parent_ob = parent_obj
wm = context.window_manager
return wm.invoke_props_dialog(self, width=400)
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
layout.prop(self, 'name')
layout.separator()
layout.prop(self, 'location', expand=True)
layout.prop(self, 'radius', slider=True)
layout.separator()
layout.prop(self, 'parent_method', expand=True)
if self.parent_method == 'MANUAL':
col = layout.column(align=True)
col.prop(context.scene, 'tweak_lattice_parent_ob')
scene = context.scene
if scene.tweak_lattice_parent_ob and scene.tweak_lattice_parent_ob.type == 'ARMATURE':
col.prop_search(self, 'parent_bone', scene.tweak_lattice_parent_ob.data, 'bones')
def execute(self, context):
scene = context.scene
# Ensure a collection to organize all our objects in.
coll = ensure_tweak_lattice_collection(context.scene)
# Create a lattice object at the 3D cursor.
lattice_name = f"LTC-{self.name}"
lattice = bpy.data.lattices.new(lattice_name)
lattice_ob = bpy.data.objects.new(lattice_name, lattice)
coll.objects.link(lattice_ob)
# Set resolution
set_lattice_resolution(lattice_ob, *self.resolution)
lattice_ob.hide_viewport = True
# Create a falloff vertex group.
vg = ensure_falloff_vgroup(lattice_ob, vg_name="Hook", func=FALLOFF_TYPES['SMOOTH'])
# Create the Hook Empty.
hook_name = "Hook_" + lattice_ob.name
hook = bpy.data.objects.new(hook_name, None)
hook.empty_display_type = 'SPHERE'
hook.empty_display_size = 0.5
coll.objects.link(hook)
# Create some custom properties.
hook['Lattice'] = lattice_ob
lattice_ob['Hook'] = hook
hook['Strength'] = 1.0
rna_idprop_ui_create(
hook,
"Influence",
default=1.0,
min=0,
max=1,
description="Influence of this lattice on all of its target objects",
)
rna_idprop_ui_create(
hook,
"Radius",
default=self.radius,
min=0,
soft_max=0.2,
max=100,
description="Size of the influenced area",
)
# Create a Root Empty to parent both the hook and the lattice to.
# This will allow pressing Alt+G/R/S on the hook to reset its transforms.
root_name = "Root_" + hook.name
root = bpy.data.objects.new(root_name, None)
root['Hook'] = hook
root.empty_display_type = 'CUBE'
root.empty_display_size = 0.5
coll.objects.link(root)
hook['Root'] = root
self.set_parent_and_transform(context, root)
# Disable the root from view.
# NOTE: This must be done AFTER any reliance on view_layer.update() calls!
# They don't work when the object is disabled!
root.hide_viewport = True
# Parent lattice and hook to root.
lattice_ob.parent = root
hook.parent = root
# Add Hook modifier to the lattice.
hook_mod = lattice_ob.modifiers.new(name="Hook", type='HOOK')
hook_mod.object = hook
hook_mod.vertex_group = vg.name
# Add Lattice modifier to the selected objects
add_objects_to_lattice(hook, context.selected_objects)
# Set up Radius control.
add_radius_constraint(hook, hook, root)
add_radius_constraint(lattice_ob, hook, root)
root_drv = simple_driver(root, 'empty_display_size', hook, '["Radius"]')
root_drv.expression = 'var/2'
# Deselect everything, select the hook and make it active
bpy.ops.object.select_all(action='DESELECT')
hook.select_set(True)
context.view_layer.objects.active = hook
# Clear the parent selector helper property.
scene.tweak_lattice_parent_ob = None
return {'FINISHED'}
def set_parent_and_transform(self, context, root):
scene = context.scene
depsgraph = context.evaluated_depsgraph_get()
matrix_of_parent = self.get_lattice_parent_matrix(context)
root.matrix_world = matrix_of_parent.copy()
context.view_layer.update()
mat_pre_arm_con = root.matrix_world.copy()
if self.parent_method == 'AUTO':
# Parent to the nearest deforming vertex.
nearest_vertex = get_nearest_evaluated_vertex(
dg=depsgraph,
coord=matrix_of_parent.copy().to_translation(),
objs=context.selected_objects,
)
obj, eval_obj, vert_idx, _vert_co = nearest_vertex
root.parent = obj.find_armature() or obj
weights = get_deforming_weights(obj, eval_obj, vert_idx)
else:
root.parent = scene.tweak_lattice_parent_ob
weights = {}
if self.parent_bone:
weights = {self.parent_bone: 1.0}
if weights:
arm_con = root.constraints.new(type='ARMATURE')
for bone_name, weight in weights.items():
tgt = arm_con.targets.new()
tgt.target = root.parent
tgt.subtarget = bone_name
tgt.weight = weight
context.view_layer.update()
mat_post_arm_con = root.matrix_world.copy()
delta = mat_pre_arm_con.inverted() @ mat_post_arm_con
root.matrix_world = matrix_of_parent @ delta.inverted()
if self.parent_method != 'AUTO' or self.location != 'CURSOR':
root.rotation_euler = 0, 0, 0
def get_lattice_parent_matrix(self, context):
location = self.location
parent_bone = self.parent_bone
scene = context.scene
if location == 'CENTER':
meshes = [o for o in context.selected_objects if o.type == 'MESH']
mat = Matrix.Identity((4))
mat.translation = bounding_box_center_of_objects(meshes)
return mat
elif location == 'CURSOR':
return context.scene.cursor.matrix.copy()
elif location == 'PARENT':
ob_mat = scene.tweak_lattice_parent_ob.matrix_world
if parent_bone:
bone_mat = scene.tweak_lattice_parent_ob.pose.bones[parent_bone].matrix
return ob_mat @ bone_mat
else:
return ob_mat
class OBJECT_OT_tweaklattice_duplicate(Operator):
"""Duplicate this Tweak Lattice set-up"""
bl_idname = "lattice.duplicate_tweak_setup"
bl_label = "Duplicate Tweak Lattice"
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
def execute(self, context):
hook, lattice, root = get_tweak_setup(context.object)
bpy.ops.object.select_all(action='DESELECT')
affected_objects = get_objects_of_lattice(hook)
visibilities = {}
for ob in [hook, lattice, root]:
ob.hide_set(False)
visibilities[ob] = ob.hide_viewport
ob.hide_viewport = False
if not ob.visible_get():
self.report({'ERROR'}, f'Object "{ob.name}" could not be made visible, cancelling.')
return {'CANCELLED'}
ob.select_set(True)
context.view_layer.objects.active = hook
bpy.ops.object.duplicate()
new_hook, new_lattice, new_root = get_tweak_setup(context.object)
for key, value in list(new_hook.items()):
if key.startswith("object_"):
del new_hook[key]
add_objects_to_lattice(new_hook, affected_objects)
# Restore visibilities
for ob, new_ob in zip((hook, lattice, root), (new_hook, new_lattice, new_root)):
ob.hide_viewport = new_ob.hide_viewport = visibilities[ob]
return {'FINISHED'}
class TweakLatticeProperties(PropertyGroup):
def update_falloff(self, context):
falloff_func = FALLOFF_TYPES[self.falloff_type]
hook, lattice, _root = get_tweak_setup(context.active_object)
ensure_falloff_vgroup(lattice, 'Hook', multiplier=self.strength, func=falloff_func)
hook['Strength'] = self.strength
strength: FloatProperty(
name="Strength",
description="A multiplier on the weight values",
default=1,
update=update_falloff,
min=0,
soft_max=2,
)
falloff_type: EnumProperty(
name="Falloff Shape",
description="Choose shape of influence for this lattice",
items=[
('LINEAR', 'Linear', 'Linear'),
('CONSTANT', 'Constant', 'Constant'),
('SHARP', 'Sharp', 'Sharp'),
('ROOT', 'Root', 'Root'),
('SMOOTH', 'Smooth', 'Smooth'),
('SPHERE', 'Sphere', 'Sphere'),
('DONUT', 'Donut', 'Donut'),
],
default='SMOOTH',
update=update_falloff,
)
class OBJECT_OT_tweaklattice_delete(Operator):
"""Delete a tweak lattice setup with all its helper objects, drivers, etc"""
bl_idname = "lattice.delete_tweak_lattice"
bl_label = "Delete Tweak Lattice"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
hook, lattice, root = get_tweak_setup(context.object)
if hook and lattice and root:
return True
cls.poll_message_set("Tweak Lattice set-up is incomplete. Some objects were manually deleted.")
return False
def execute(self, context):
hook, lattice, root = get_tweak_setup(context.object)
# Remove Lattice modifiers and their drivers.
remove_all_objects_from_lattice(hook)
# Remove hook Action if exists.
if hook.animation_data and hook.animation_data.action:
bpy.data.actions.remove(hook.animation_data.action)
# Remove objects and Lattice datablock.
bpy.data.objects.remove(hook)
lattice_data = lattice.data
bpy.data.objects.remove(lattice)
bpy.data.lattices.remove(lattice_data)
bpy.data.objects.remove(root)
# Remove the collection if it's empty.
coll = bpy.data.collections.get(TWEAKLAT_COLL_NAME)
if coll and len(coll.all_objects) == 0:
bpy.data.collections.remove(coll)
return {'FINISHED'}
class OBJECT_OT_tweaklattice_objects_add(Operator):
"""Add selected objects to this tweak lattice"""
bl_idname = "lattice.add_selected_objects"
bl_label = "Add Selected Objects"
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
@classmethod
def poll(cls, context):
hook, _lattice, _root = get_tweak_setup(context.object)
if not hook:
cls.poll_message_set("Cannot find hook object of this Tweak Lattice set-up. Perhaps it was deleted?")
return False
values = hook.values()
for sel_o in context.selected_objects:
if sel_o == hook or sel_o.type != 'MESH':
continue
if sel_o not in values:
return True
cls.poll_message_set("No selected objects to add to this Tweak Lattice.")
return False
def execute(self, context):
hook, _lattice, _root = get_tweak_setup(context.object)
# Add Lattice modifier to the selected objects
add_objects_to_lattice(hook, context.selected_objects)
return {'FINISHED'}
class OBJECT_OT_tweaklattice_objects_remove(Operator):
"""Remove selected objects from this tweak lattice"""
bl_idname = "lattice.remove_selected_objects"
bl_label = "Remove Selected Objects"
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
@classmethod
def poll(cls, context):
hook, _lattice, _root = get_tweak_setup(context.object)
if not hook:
cls.poll_message_set("Cannot find hook object of this Tweak Lattice set-up. Perhaps it was deleted?")
return False
values = hook.values()
for sel_o in context.selected_objects:
if sel_o == hook or sel_o.type != 'MESH':
continue
if sel_o in values:
return True
cls.poll_message_set("No selected objects to remove from this Tweak Lattice.")
return False
def execute(self, context):
hook, _lattice, _root = get_tweak_setup(context.object)
# Add Lattice modifier to the selected objects
remove_objects_from_lattice(hook, context.selected_objects)
return {'FINISHED'}
class OBJECT_OT_tweaklattice_object_remove_single(Operator):
"""Remove this object from the tweak lattice"""
bl_idname = "lattice.remove_object"
bl_label = "Remove Object"
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
ob_pointer_prop_name: StringProperty(
description="Name of the custom property that references the object that should be removed"
)
def execute(self, context):
hook, _lattice, _root = get_tweak_setup(context.object)
target = hook[self.ob_pointer_prop_name]
# Add Lattice modifier to the selected objects
remove_objects_from_lattice(hook, [target])
return {'FINISHED'}
class VIEW3D_PT_tweak_lattice(Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Lattice Magic'
bl_label = "Tweak Lattice"
@classmethod
def poll(cls, context):
hook, _lattice, _root = get_tweak_setup(context.object)
return context.object and context.object.type == 'MESH' or hook
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
hook, lattice, root = get_tweak_setup(context.object)
layout = layout.column()
if not hook:
layout.operator(OBJECT_OT_tweaklattice_create.bl_idname, icon='OUTLINER_OB_LATTICE')
return
layout.prop(hook, '["Influence"]', slider=True, text="Influence")
layout.prop(hook["Lattice"].data.lattice_magic, 'strength')
layout.separator()
layout.prop(hook["Lattice"].data.lattice_magic, 'falloff_type')
layout.prop(hook, '["Radius"]', slider=True)
layout.separator()
layout.operator(
OBJECT_OT_tweaklattice_delete.bl_idname, text='Delete Tweak Lattice', icon='TRASH'
)
layout.operator(
OBJECT_OT_tweaklattice_duplicate.bl_idname,
text='Duplicate Tweak Lattice',
icon='DUPLICATE',
)
layout.separator()
layout.label(text="Helper Objects")
lattice_row = layout.row()
lattice_row.prop(hook, '["Lattice"]', text="Lattice")
lattice_row.prop(lattice, 'hide_viewport', text="", emboss=False)
root_row = layout.row()
root_row.prop(hook, '["Root"]', text="Root")
root_row.prop(root, 'hide_viewport', text="", emboss=False)
layout.separator()
layout.label(text="Parenting")
col = layout.column()
col.enabled = False
col.prop(root, 'parent')
if root.parent and root.parent.type == 'ARMATURE':
col.prop(root, 'parent_bone', icon='BONE_DATA')
layout.separator()
layout.label(text="Affected Objects")
num_to_add = 0
for o in context.selected_objects:
if o == hook or o.type != 'MESH':
continue
if o in hook.values():
continue
num_to_add += 1
if num_to_add == 1:
text = f"Add {o.name}"
if num_to_add:
if num_to_add > 1:
text = f"Add {num_to_add} Objects"
layout.operator(OBJECT_OT_tweaklattice_objects_add.bl_idname, icon='ADD', text=text)
layout.separator()
num_to_remove = False
for o in context.selected_objects:
if o == hook or o.type != 'MESH':
continue
if o not in hook.values():
continue
num_to_remove += 1
if num_to_remove == 1:
text = f"Remove {o.name}"
if num_to_remove:
if num_to_remove > 1:
text = f"Remove {num_to_remove} Objects"
layout.operator(
OBJECT_OT_tweaklattice_objects_remove.bl_idname, icon='REMOVE', text=text
)
objects_and_keys = [(hook[key], key) for key in hook.keys() if "object_" in key]
objects_and_keys.sort(key=lambda o_and_k: o_and_k[1])
for ob, key in objects_and_keys:
row = layout.row(align=True)
row.prop(hook, f'["{key}"]', text="")
mod = get_lattice_modifier_of_object(ob, lattice)
if not mod:
continue
row.prop_search(mod, 'vertex_group', ob, 'vertex_groups', text="", icon='GROUP_VERTEX')
op = row.operator(
OBJECT_OT_tweaklattice_object_remove_single.bl_idname, text="", icon='X'
)
op.ob_pointer_prop_name = key
def set_lattice_resolution(lat_ob: Object, res_u, res_v=None, res_w=None):
assert lat_ob.type == 'LATTICE', f"This isn't a lattice object: {lat_ob.name}"
if not res_v:
res_v = res_u
if not res_w:
res_w = res_u
lat_ob.data.points_u = res_u
lat_ob.data.points_v = res_v
lat_ob.data.points_w = res_w
def build_kdtree(obj):
# Get the vertices of the mesh in world coordinates
vertices = [obj.matrix_world @ v.co for v in obj.data.vertices]
# Build KD-Tree from the vertices
size = len(vertices)
kd = kdtree.KDTree(size)
for i, vertex in enumerate(vertices):
kd.insert(vertex, i)
kd.balance()
return kd
def get_nearest_evaluated_vertex(
dg, coord: Vector, objs: list[Object]
) -> tuple[Object, int, Vector]:
"""Get nearest EVALUATED vertex to a coordinate out of a list of passed mesh objects.
Return the original object, and the evaluated object, vertex index, and coordinate.
"""
nearest_vertex = None
nearest_distance = float('inf')
for obj in objs:
if obj.type != 'MESH':
continue
eval_ob = obj.evaluated_get(dg)
kd = build_kdtree(eval_ob)
# Find the nearest vertex to the 3D cursor
eval_co, eval_idx, dist = kd.find(coord)
# If this vertex is closer than any previously found, store it
if dist < nearest_distance:
nearest_distance = dist
nearest_vertex = (obj, eval_ob, eval_idx, eval_co)
return nearest_vertex
def get_deforming_weights(obj: Object, eval_obj, vert_idx: int) -> dict[str, float] | None:
armature = obj.find_armature()
if not armature:
return
vertex = eval_obj.data.vertices[vert_idx]
weights = {}
# Loop through the vertex groups the vertex belongs to
for group in vertex.groups:
group_index = group.group
group_weight = group.weight
group_name = obj.vertex_groups[group_index].name
pbone = armature.pose.bones.get(group_name)
if pbone and pbone.bone.use_deform:
weights[group_name] = group_weight
return weights
def get_tweak_setup(obj: Object) -> Tuple[Object, Object, Object]:
"""Based on either the hook, lattice or root, return all three."""
if not obj:
return [None, None, None]
if obj.type == 'EMPTY':
if 'Root' and 'Lattice' in obj:
return obj, obj['Lattice'], obj['Root']
elif 'Hook' in obj:
return obj['Hook'], obj['Hook']['Lattice'], obj
elif obj.type == 'LATTICE' and 'Hook' in obj:
return obj['Hook'], obj, obj['Hook']['Root']
return [None, None, None]
def ensure_tweak_lattice_collection(scene: Scene) -> Collection:
coll = bpy.data.collections.get(TWEAKLAT_COLL_NAME)
if not coll:
coll = bpy.data.collections.new(TWEAKLAT_COLL_NAME)
scene.collection.children.link(coll)
return coll
def ensure_falloff_vgroup(
lattice_ob: Object, vg_name="Group", multiplier=1, func=lambda x: x
) -> VertexGroup:
lattice = lattice_ob.data
res_x, res_y, res_z = lattice.points_u, lattice.points_v, lattice.points_w
vg = lattice_ob.vertex_groups.get(vg_name)
center = Vector((res_x - 1, res_y - 1, res_z - 1)) / 2
max_res = max(res_x, res_y, res_z)
if not vg:
vg = lattice_ob.vertex_groups.new(name=vg_name)
for x in range(res_x - 4):
for y in range(res_y - 4):
for z in range(res_z - 4):
index = get_lattice_vertex_index(lattice, (x + 2, y + 2, z + 2))
coord = Vector((x + 2, y + 2, z + 2))
distance_from_center = (coord - center).length
distance_factor = clamp(1 - (distance_from_center / max_res * 2), 0, 1)
influence = func(distance_factor)
vg.add([index], influence * multiplier, 'REPLACE')
return vg
def add_radius_constraint(obj, hook, target):
trans_con = obj.constraints.new(type='TRANSFORM')
trans_con.name += " (Radius Scaling)"
trans_con.target = target
trans_con.map_to = 'SCALE'
trans_con.mix_mode_scale = 'MULTIPLY'
for prop in ['to_min_x_scale', 'to_min_y_scale', 'to_min_z_scale']:
simple_driver(trans_con, prop, hook, '["Radius"]')
return trans_con
def get_objects_of_lattice(hook: Object) -> List[Object]:
objs = []
for key, value in hook.items():
if key.startswith("object_") and value:
objs.append(value)
return objs
def get_lattice_modifier_of_object(obj, lattice) -> Modifier:
"""Find the lattice modifier on the object that uses this lattice"""
if not obj:
return
for m in obj.modifiers:
if m.type == 'LATTICE' and m.object == lattice:
return m
def add_objects_to_lattice(hook: Object, objects: List[Object]):
lattice_ob = hook['Lattice']
for i, obj in enumerate(objects):
obj.select_set(False)
if obj.type != 'MESH' or obj in hook.values():
continue
# Make sure overridden object is editable.
if obj.override_library:
obj.override_library.is_system_override = False
mod = obj.modifiers.new(name=lattice_ob.name, type='LATTICE')
mod.object = lattice_ob
# Make sure the property name is available.
offset = 0
while "object_" + str(offset) in hook:
offset += 1
hook["object_" + str(i + offset)] = obj
# Add driver to the modifier influence.
simple_driver(mod, 'strength', hook, '["Influence"]')
def remove_object_from_lattice(hook: Object, obj: Object):
"""Cleanly remove an object from a Tweak Lattice set-up's influence."""
hook, lattice, root = get_tweak_setup(hook)
# Remove the custom property pointing from the Hook to the Object.
for key, value in list(hook.items()):
if value == obj:
del hook[key]
break
# Remove the Lattice modifier (and its driver) deforming the Object.
for m in obj.modifiers:
if m.type != 'LATTICE':
continue
if m.object == lattice:
m.driver_remove('strength')
obj.modifiers.remove(m)
break
def remove_objects_from_lattice(hook: Object, objects_to_remove: List[Object]) -> List[Object]:
"""Cleanly remove several objects from a Tweak Lattice set-up's influence."""
objs_removed = []
for key, value in list(hook.items()):
if value in objects_to_remove:
remove_object_from_lattice(hook, value)
objs_removed.append(value)
return objs_removed
def remove_all_objects_from_lattice(hook: Object) -> List[Object]:
"""Cleanly remove all objects from a Tweak Lattice set-up's influence."""
objs_to_remove = []
for key, value in list(hook.items()):
if key.startswith("object_"):
objs_to_remove.append(value)
return remove_objects_from_lattice(hook, objs_to_remove)
registry = [
OBJECT_OT_tweaklattice_create,
OBJECT_OT_tweaklattice_duplicate,
OBJECT_OT_tweaklattice_delete,
OBJECT_OT_tweaklattice_objects_add,
OBJECT_OT_tweaklattice_objects_remove,
OBJECT_OT_tweaklattice_object_remove_single,
VIEW3D_PT_tweak_lattice,
TweakLatticeProperties,
]
def register():
Scene.tweak_lattice_parent_ob = PointerProperty(type=Object, name="Parent")
Lattice.lattice_magic = PointerProperty(type=TweakLatticeProperties)
def unregister():
del Scene.tweak_lattice_parent_ob
del Lattice.lattice_magic