Add Lattice Magic to Addons #48

Merged
Nick Alberelli merged 36 commits from feature/lattice_magic into main 2023-05-17 20:48:52 +02:00
5 changed files with 248 additions and 225 deletions
Showing only changes of commit cbf891eba5 - Show all commits

View File

@ -27,20 +27,20 @@ bl_info = {
from . import camera_lattice
from . import tweak_lattice
from . import operators
import importlib
modules = [
camera_lattice
,tweak_lattice
,operators
]
def register():
for m in modules:
importlib.reload(m)
if hasattr(m, 'register'):
m.register()
m.register()
def unregister():
for m in modules:
if hasattr(m, 'unregister'):
m.unregister()
m.unregister()

View File

@ -8,33 +8,12 @@
# 3D Lattices: Need to have a distance, thickness and Z resolution parameter.
import bpy
from typing import Tuple, List
import math
from mathutils import Vector
from bpy.props import BoolProperty, PointerProperty, CollectionProperty, IntProperty, EnumProperty, FloatProperty
from mathutils.geometry import intersect_point_line
def bounding_box(points) -> Tuple[Vector, Vector]:
""" Return two vectors representing the lowest and highest coordinates of
a the bounding box of the passed points.
"""
lowest = points[0].copy()
highest = points[0].copy()
for p in points:
for i in range(len(p)):
if p[i] < lowest[i]:
lowest[i] = p[i]
if p[i] > highest[i]:
highest[i] = p[i]
return lowest, highest
def bounding_box_center(points) -> Vector:
"""Find the bounding box center of some points."""
bbox_low, bbox_high = bounding_box(points)
return bbox_low + (bbox_high-bbox_low)/2
from .utils import bounding_box_center, bounding_box
class CAMLAT_UL_lattice_slots(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
@ -102,6 +81,26 @@ class LatticeSlot(bpy.types.PropertyGroup):
)
class CAMLAT_OT_Add(bpy.types.Operator):
"""Add a Camera Lattice Slot"""
bl_idname = "lattice.add_slot"
bl_label = "Add Lattice Slot"
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
def execute(self, context):
scene = context.scene
lattice_slots = scene.lattice_slots
active_index = scene.active_lattice_index
to_index = active_index + 1
if len(lattice_slots)==0:
to_index = 0
scene.lattice_slots.add()
scene.lattice_slots.move(len(scene.lattice_slots)-1, to_index)
scene.active_lattice_index = to_index
return { 'FINISHED' }
class CAMLAT_OT_Remove(bpy.types.Operator):
"""Remove Lattice Slot along with its Lattice object, animation and modifiers"""
bl_idname = "lattice.remove_slot"
@ -132,26 +131,6 @@ class CAMLAT_OT_Remove(bpy.types.Operator):
return { 'FINISHED' }
class CAMLAT_OT_Add(bpy.types.Operator):
"""Add a Camera Lattice Slot"""
bl_idname = "lattice.add_slot"
bl_label = "Add Lattice Slot"
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
def execute(self, context):
scene = context.scene
lattice_slots = scene.lattice_slots
active_index = scene.active_lattice_index
to_index = active_index + 1
if len(lattice_slots)==0:
to_index = 0
scene.lattice_slots.add()
scene.lattice_slots.move(len(scene.lattice_slots)-1, to_index)
scene.active_lattice_index = to_index
return { 'FINISHED' }
class CAMLAT_OT_Move(bpy.types.Operator):
"""Move Lattice Slot"""
bl_idname = "lattice.move_slot"
@ -364,41 +343,6 @@ class CAMLAT_OT_Delete(bpy.types.Operator):
return { 'FINISHED' }
class ShapeKey_OT_Reset(bpy.types.Operator):
"""Reset shape of the active shape key of the active object"""
bl_idname = "object.reset_shape_key"
bl_label = "Reset Shape Key"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
ob = context.object
if not ob:
return False
if not ob.data.shape_keys:
return False
if len(ob.data.shape_keys.key_blocks)<2:
return False
if ob.active_shape_key_index==0:
return False
return True
def execute(self, context):
ob = context.object
active_index = ob.active_shape_key_index
key_blocks = ob.data.shape_keys.key_blocks
active_block = key_blocks[active_index]
basis_block = key_blocks[0]
for i, skp in enumerate(active_block.data):
skp.co = basis_block.data[i].co
return { 'FINISHED' }
def draw_shape_key_reset(self, context):
self.layout.operator(ShapeKey_OT_Reset.bl_idname, text="Reset Shape Key", icon='FILE_REFRESH')
class CAMLAT_OT_ShapeKey_Add(bpy.types.Operator):
"""Add a shape key to the active Lattice Slot's lattice, named after the current frame number"""
@ -422,11 +366,12 @@ class CAMLAT_OT_ShapeKey_Add(bpy.types.Operator):
return {'FINISHED'}
class CAMLAT_PT_main(bpy.types.Panel):
class CAMLAT_PT_Main(bpy.types.Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Lattice Magic'
bl_label = "Camera Lattice Slots"
bl_label = "Camera Lattice"
@classmethod
def poll(cls, context):
@ -523,15 +468,16 @@ class CAMLAT_PT_main(bpy.types.Panel):
classes = [
LatticeSlot
,CAMLAT_UL_lattice_slots
,CAMLAT_PT_main
,CAMLAT_OT_Remove
,CAMLAT_OT_Add
,CAMLAT_OT_Remove
,CAMLAT_OT_Move
,CAMLAT_OT_Generate
,CAMLAT_OT_Delete
,ShapeKey_OT_Reset
,CAMLAT_OT_ShapeKey_Add
,CAMLAT_PT_Main
]
def register():
@ -541,7 +487,6 @@ def register():
bpy.types.Scene.lattice_slots = CollectionProperty(type=LatticeSlot)
bpy.types.Scene.active_lattice_index = IntProperty()
bpy.types.MESH_MT_shape_key_context_menu.append(draw_shape_key_reset)
def unregister():
from bpy.utils import unregister_class
@ -550,4 +495,3 @@ def unregister():
del bpy.types.Scene.lattice_slots
del bpy.types.Scene.active_lattice_index
bpy.types.MESH_MT_shape_key_context_menu.remove(draw_shape_key_reset)

113
operators.py Normal file
View File

@ -0,0 +1,113 @@
import bpy
from bpy.props import FloatProperty
from .utils import get_lattice_point_original_position
class LATTICE_OT_Reset(bpy.types.Operator):
"""Reset selected lattice points to their default position."""
bl_idname = "lattice.reset_points"
bl_label = "Reset Lattice Points"
bl_options = {'REGISTER', 'UNDO'}
factor: FloatProperty(name="Factor", min=0, max=1, default=1)
@classmethod
def poll(cls, context):
return len(context.selected_objects)>0 and context.mode=='EDIT_LATTICE'
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.prop(self, 'factor', slider=True)
def execute(self, context):
bpy.ops.object.mode_set(mode='OBJECT')
for ob in context.selected_objects:
if ob.type!='LATTICE':
continue
# Resetting shape key or Basis shape
if ob.data.shape_keys:
active_index = ob.active_shape_key_index
key_blocks = ob.data.shape_keys.key_blocks
active_block = key_blocks[active_index]
basis_block = key_blocks[0]
if active_index > 0:
for i, skp in enumerate(active_block.data):
point = ob.data.points[i]
if not point.select: continue
mix = skp.co.lerp(basis_block.data[i].co, self.factor)
skp.co = mix
continue
else:
for i, skp in enumerate(active_block.data):
base = get_lattice_point_original_position(ob.data, i)
# Resetting the Basis shape
mix = basis_block.data[i].co.lerp(base, self.factor)
basis_block.data[i].co = mix
continue
# Otherwise, reset the actual points.
for i in range(len(ob.data.points)):
point = ob.data.points[i]
if not point.select:
continue
base = get_lattice_point_original_position(ob.data, i)
mix = point.co_deform.lerp(base, self.factor)
point.co_deform = base
bpy.ops.object.mode_set(mode='EDIT')
return {'FINISHED'}
class ShapeKey_OT_Reset(bpy.types.Operator):
"""Reset shape of the active shape key of the active object"""
bl_idname = "object.reset_shape_key"
bl_label = "Reset Shape Key"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
ob = context.object
if not ob:
return False
if not ob.data.shape_keys:
return False
if len(ob.data.shape_keys.key_blocks)<2:
return False
if ob.active_shape_key_index==0:
return False
return True
def execute(self, context):
ob = context.object
active_index = ob.active_shape_key_index
key_blocks = ob.data.shape_keys.key_blocks
active_block = key_blocks[active_index]
basis_block = key_blocks[0]
for i, skp in enumerate(active_block.data):
skp.co = basis_block.data[i].co
return { 'FINISHED' }
def draw_shape_key_reset(self, context):
self.layout.operator(ShapeKey_OT_Reset.bl_idname, text="Reset Shape Key", icon='FILE_REFRESH')
classes = [
ShapeKey_OT_Reset
,LATTICE_OT_Reset
]
def register():
from bpy.utils import register_class
for c in classes:
register_class(c)
bpy.types.MESH_MT_shape_key_context_menu.append(draw_shape_key_reset)
def unregister():
from bpy.utils import unregister_class
for c in reversed(classes):
unregister_class(c)
bpy.types.MESH_MT_shape_key_context_menu.remove(draw_shape_key_reset)

View File

@ -21,79 +21,10 @@ from typing import List
from mathutils import Vector
from rna_prop_ui import rna_idprop_ui_create
from .utils import clamp, get_lattice_vertex_index, simple_driver
coll_name = 'Tweak Lattices'
def clamp(val, _min=0, _max=1) -> float or int:
if val < _min:
return _min
if val > _max:
return _max
return val
def get_lattice_vertex_index(lattice: bpy.types.Lattice, xyz: List[int], do_clamp=True) -> int:
"""Get the index of a lattice vertex based on its position on the XYZ axes."""
# The lattice vertex indicies start in the -Y, -X, -Z corner,
# increase on X+, then moves to the next row on Y+, then moves up on Z+.
res_x, res_y, res_z = lattice.points_u, lattice.points_v, lattice.points_w
x, y, z = xyz[:]
if do_clamp:
x = clamp(x, 0, res_x)
y = clamp(y, 0, res_y)
z = clamp(z, 0, res_z)
assert x < res_x and y < res_y and z < res_z, "Error: Lattice vertex xyz index out of bounds"
index = (z * res_y*res_x) + (y * res_x) + x
return index
def get_lattice_vertex_xyz_position(lattice: bpy.types.Lattice, index: int) -> (int, int, int):
res_x, res_y, res_z = lattice.points_u, lattice.points_v, lattice.points_w
x = 0
remaining = index
z = int(remaining / (res_x*res_y))
remaining -= z*(res_x*res_y)
y = int(remaining / res_x)
remaining -= y*res_x
x = remaining # Maybe need to add or subtract 1 here?
return (x, y, z)
def get_lattice_point_original_position(lattice: bpy.types.Lattice, index: int) -> Vector:
"""Reset a lattice vertex to its original position."""
start_vec = Vector((-0.5, -0.5, -0.5))
if lattice.points_u == 1:
start_vec[0] = 0
if lattice.points_v == 1:
start_vec[1] = 0
if lattice.points_w == 1:
start_vec[2] = 0
unit_u = 1/(lattice.points_u-1)
unit_v = 1/(lattice.points_v-1)
unit_w = 1/(lattice.points_w-1)
unit_vec = Vector((unit_u, unit_v, unit_w))
xyz_vec = Vector(get_lattice_vertex_xyz_position(lattice, index))
return start_vec + xyz_vec*unit_vec
def simple_driver(owner: bpy.types.ID, driver_path: str, target_ob: bpy.types.Object, data_path: str, array_index=-1) -> bpy.types.Driver:
if array_index > -1:
owner.driver_remove(driver_path, array_index)
driver = owner.driver_add(driver_path, array_index).driver
else:
owner.driver_remove(driver_path)
driver = owner.driver_add(driver_path).driver
driver.expression = 'var'
var = driver.variables.new()
var.targets[0].id = target_ob
var.targets[0].data_path = data_path
return driver
def ensure_tweak_lattice_collection(scene):
coll = bpy.data.collections.get(coll_name)
if not coll:
@ -102,62 +33,6 @@ def ensure_tweak_lattice_collection(scene):
return coll
class LATTICE_OT_Reset(bpy.types.Operator):
"""Reset selected lattice points to their default position."""
bl_idname = "lattice.reset_points"
bl_label = "Reset Lattice Points"
bl_options = {'REGISTER', 'UNDO'}
factor: FloatProperty(name="Factor", min=0, max=1, default=1)
@classmethod
def poll(cls, context):
return len(context.selected_objects)>0 and context.mode=='EDIT_LATTICE'
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.prop(self, 'factor', slider=True)
def execute(self, context):
bpy.ops.object.mode_set(mode='OBJECT')
for ob in context.selected_objects:
if ob.type!='LATTICE':
continue
# Resetting shape key or Basis shape
if ob.data.shape_keys:
active_index = ob.active_shape_key_index
key_blocks = ob.data.shape_keys.key_blocks
active_block = key_blocks[active_index]
basis_block = key_blocks[0]
if active_index > 0:
for i, skp in enumerate(active_block.data):
point = ob.data.points[i]
if not point.select: continue
mix = skp.co.lerp(basis_block.data[i].co, self.factor)
skp.co = mix
continue
else:
for i, skp in enumerate(active_block.data):
base = get_lattice_point_original_position(ob.data, i)
# Resetting the Basis shape
mix = basis_block.data[i].co.lerp(base, self.factor)
basis_block.data[i].co = mix
continue
# Otherwise, reset the actual points.
for i in range(len(ob.data.points)):
point = ob.data.points[i]
if not point.select:
continue
base = get_lattice_point_original_position(ob.data, i)
mix = point.co_deform.lerp(base, self.factor)
point.co_deform = base
bpy.ops.object.mode_set(mode='EDIT')
return {'FINISHED'}
class TWEAKLAT_OT_Create(bpy.types.Operator):
"""Create a lattice setup at the 3D cursor to deform selected objects."""
bl_idname = "lattice.create_tweak_lattice"
@ -330,7 +205,7 @@ class TWEAKLAT_OT_Delete(bpy.types.Operator):
return {'FINISHED'}
class TWEAKLAT_PT_main(bpy.types.Panel):
class TWEAKLAT_PT_Main(bpy.types.Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Lattice Magic'
@ -377,10 +252,9 @@ class TWEAKLAT_PT_main(bpy.types.Panel):
ob_prop_name = "object_"+str(ob_count)
classes = [
LATTICE_OT_Reset
,TWEAKLAT_OT_Create
TWEAKLAT_OT_Create
,TWEAKLAT_OT_Delete
,TWEAKLAT_PT_main
,TWEAKLAT_PT_Main
]
def register():
@ -388,11 +262,7 @@ def register():
for c in classes:
register_class(c)
# bpy.types.MESH_MT_shape_key_context_menu.append(draw_shape_key_reset)
def unregister():
from bpy.utils import unregister_class
for c in reversed(classes):
unregister_class(c)
# bpy.types.MESH_MT_shape_key_context_menu.remove(draw_shape_key_reset)

96
utils.py Normal file
View File

@ -0,0 +1,96 @@
import bpy
from mathutils import Vector
from typing import List, Tuple
def clamp(val, _min=0, _max=1) -> float or int:
if val < _min:
return _min
if val > _max:
return _max
return val
def get_lattice_vertex_index(lattice: bpy.types.Lattice, xyz: List[int], do_clamp=True) -> int:
"""Get the index of a lattice vertex based on its position on the XYZ axes."""
# The lattice vertex indicies start in the -Y, -X, -Z corner,
# increase on X+, then moves to the next row on Y+, then moves up on Z+.
res_x, res_y, res_z = lattice.points_u, lattice.points_v, lattice.points_w
x, y, z = xyz[:]
if do_clamp:
x = clamp(x, 0, res_x)
y = clamp(y, 0, res_y)
z = clamp(z, 0, res_z)
assert x < res_x and y < res_y and z < res_z, "Error: Lattice vertex xyz index out of bounds"
index = (z * res_y*res_x) + (y * res_x) + x
return index
def get_lattice_vertex_xyz_position(lattice: bpy.types.Lattice, index: int) -> (int, int, int):
res_x, res_y, res_z = lattice.points_u, lattice.points_v, lattice.points_w
x = 0
remaining = index
z = int(remaining / (res_x*res_y))
remaining -= z*(res_x*res_y)
y = int(remaining / res_x)
remaining -= y*res_x
x = remaining # Maybe need to add or subtract 1 here?
return (x, y, z)
def get_lattice_point_original_position(lattice: bpy.types.Lattice, index: int) -> Vector:
"""Reset a lattice vertex to its original position."""
start_vec = Vector((-0.5, -0.5, -0.5))
if lattice.points_u == 1:
start_vec[0] = 0
if lattice.points_v == 1:
start_vec[1] = 0
if lattice.points_w == 1:
start_vec[2] = 0
unit_u = 1/(lattice.points_u-1)
unit_v = 1/(lattice.points_v-1)
unit_w = 1/(lattice.points_w-1)
unit_vec = Vector((unit_u, unit_v, unit_w))
xyz_vec = Vector(get_lattice_vertex_xyz_position(lattice, index))
return start_vec + xyz_vec*unit_vec
def simple_driver(owner: bpy.types.ID, driver_path: str, target_ob: bpy.types.Object, data_path: str, array_index=-1) -> bpy.types.Driver:
if array_index > -1:
owner.driver_remove(driver_path, array_index)
driver = owner.driver_add(driver_path, array_index).driver
else:
owner.driver_remove(driver_path)
driver = owner.driver_add(driver_path).driver
driver.expression = 'var'
var = driver.variables.new()
var.targets[0].id = target_ob
var.targets[0].data_path = data_path
return driver
def bounding_box(points) -> Tuple[Vector, Vector]:
""" Return two vectors representing the lowest and highest coordinates of
a the bounding box of the passed points.
"""
lowest = points[0].copy()
highest = points[0].copy()
for p in points:
for i in range(len(p)):
if p[i] < lowest[i]:
lowest[i] = p[i]
if p[i] > highest[i]:
highest[i] = p[i]
return lowest, highest
def bounding_box_center(points) -> Vector:
"""Find the bounding box center of some points."""
bbox_low, bbox_high = bounding_box(points)
return bbox_low + (bbox_high-bbox_low)/2