yyc12345
0bec108dcb
bring ballance virtools helper used fix_texture (renamed as fix material to correspond with the real object operated by this function) into blender plugin.
274 lines
10 KiB
Python
274 lines
10 KiB
Python
import bpy, mathutils
|
|
import enum, typing
|
|
from . import UTIL_functions
|
|
|
|
#region Align Mode
|
|
|
|
class AlignMode(enum.IntEnum):
|
|
Min = enum.auto()
|
|
BBoxCenter = enum.auto()
|
|
AxisCenter = enum.auto()
|
|
Max = enum.auto()
|
|
_g_AlignModeDesc: dict[AlignMode, tuple[str, str]] = {
|
|
AlignMode.Min: ("Min", "The min value in specified axis."),
|
|
AlignMode.BBoxCenter: ("Center (Bounding Box)", "The bounding box center in specified axis."),
|
|
AlignMode.AxisCenter: ("Center (Axis)", "The object's source point in specified axis."),
|
|
AlignMode.Max: ("Max", "The max value in specified axis."),
|
|
}
|
|
_g_EnumHelper_AlignMode: UTIL_functions.EnumPropHelper = UTIL_functions.EnumPropHelper(
|
|
AlignMode,
|
|
lambda x: str(x.value),
|
|
lambda x: AlignMode(int(x)),
|
|
lambda x: _g_AlignModeDesc[x][0],
|
|
lambda x: _g_AlignModeDesc[x][1],
|
|
lambda _: ''
|
|
)
|
|
|
|
#endregion
|
|
|
|
#region Align Cache Implement
|
|
|
|
## As we known, 3ds Max's align window have a Apply button which can apply current align to scene,
|
|
# and user call set next align settings after clicking Apply. It will not affect previous set align settings.
|
|
# But Blender have no vanilla Apply function for operator. The only possible way is re-run this operator.
|
|
# However the experience is pretty shit. Because the window still locate at the left-bottom corner.
|
|
# User can't keep up to change it.
|
|
#
|
|
# We use a dirty way to implement Apply function. The solution is pretty like BME struct adder.
|
|
# We use a CollectionProperty to store all align steps.
|
|
# And use a BoolProperty with update function to implement Apply button. Once its value changed,
|
|
# reset its value (order a recursive hinder), and add a new settings.
|
|
|
|
class BBP_PG_legacy_align_history(bpy.types.PropertyGroup):
|
|
align_x: bpy.props.BoolProperty(
|
|
name = "X Position",
|
|
default = False,
|
|
)
|
|
align_y: bpy.props.BoolProperty(
|
|
name = "Y Position",
|
|
default = False,
|
|
)
|
|
align_z: bpy.props.BoolProperty(
|
|
name = "Z Position",
|
|
default = False,
|
|
)
|
|
current_align_mode: bpy.props.EnumProperty(
|
|
name = "Current Object (Active Object)",
|
|
items = _g_EnumHelper_AlignMode.generate_items(),
|
|
default = _g_EnumHelper_AlignMode.to_selection(AlignMode.AxisCenter),
|
|
)
|
|
target_align_mode: bpy.props.EnumProperty(
|
|
name = "Target Objects (Other Objects)",
|
|
items = _g_EnumHelper_AlignMode.generate_items(),
|
|
default = _g_EnumHelper_AlignMode.to_selection(AlignMode.AxisCenter),
|
|
)
|
|
|
|
#endregion
|
|
|
|
class BBP_OT_legacy_align(bpy.types.Operator):
|
|
"""Align Objects with 3ds Max Style"""
|
|
bl_idname = "bbp.legacy_align"
|
|
bl_label = "3ds Max Align"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
# the updator for apply flag value
|
|
def apply_flag_updated(self, context):
|
|
# check hinder and set hinder first
|
|
if self.recursive_hinder: return
|
|
self.recursive_hinder = True
|
|
|
|
# reset apply button value (default is True)
|
|
# due to the hinder, no recursive calling will happend
|
|
if self.apply_flag == True: return
|
|
self.apply_flag = True
|
|
|
|
# check whether add new entry
|
|
# if no selected axis, this alignment is invalid
|
|
entry: BBP_PG_legacy_align_history = self.align_history[-1]
|
|
if entry.align_x == True or entry.align_y == True or entry.align_z == True:
|
|
# valid one
|
|
# add a new entry in history
|
|
self.align_history.add()
|
|
else:
|
|
# invalid one
|
|
# reset all data to default
|
|
entry.align_x = False
|
|
entry.align_y = False
|
|
entry.align_z = False
|
|
entry.current_align_mode = _g_EnumHelper_AlignMode.to_selection(AlignMode.AxisCenter)
|
|
entry.target_align_mode = _g_EnumHelper_AlignMode.to_selection(AlignMode.AxisCenter)
|
|
|
|
# reset hinder
|
|
self.recursive_hinder = False
|
|
# blender required
|
|
return None
|
|
|
|
apply_flag: bpy.props.BoolProperty(
|
|
name = "Apply Flag",
|
|
description = "Internal flag.",
|
|
options = {'HIDDEN', 'SKIP_SAVE'},
|
|
default = True, # default True value to make it as a "light" button, not a grey one.
|
|
update = apply_flag_updated,
|
|
)
|
|
recursive_hinder: bpy.props.BoolProperty(
|
|
name = "Recursive Hinder",
|
|
description = "An internal flag to prevent the loop calling to apply_flags's updator.",
|
|
options = {'HIDDEN', 'SKIP_SAVE'},
|
|
default = False,
|
|
)
|
|
align_history : bpy.props.CollectionProperty(
|
|
name = "Historys",
|
|
description = "Align history.",
|
|
type = BBP_PG_legacy_align_history,
|
|
)
|
|
|
|
@classmethod
|
|
def poll(self, context):
|
|
return _check_align_requirement()
|
|
|
|
def invoke(self, context, event):
|
|
# clear history and add 1 entry for following functions
|
|
self.align_history.clear()
|
|
self.align_history.add()
|
|
# run execute() function
|
|
return self.execute(context)
|
|
|
|
def execute(self, context):
|
|
# get processed objects
|
|
(current_obj, target_objs) = _prepare_objects()
|
|
# iterate history to align objects
|
|
entry: BBP_PG_legacy_align_history
|
|
for entry in self.align_history:
|
|
_align_objects(
|
|
current_obj, target_objs,
|
|
entry.align_x, entry.align_y, entry.align_z,
|
|
_g_EnumHelper_AlignMode.get_selection(entry.current_align_mode),
|
|
_g_EnumHelper_AlignMode.get_selection(entry.target_align_mode)
|
|
)
|
|
return {'FINISHED'}
|
|
|
|
def draw(self, context):
|
|
# get last entry in history to show
|
|
entry: BBP_PG_legacy_align_history = self.align_history[-1]
|
|
|
|
layout = self.layout
|
|
col = layout.column()
|
|
|
|
# show axis
|
|
col.label(text="Align Axis (Multi-selection)")
|
|
row = col.row()
|
|
row.prop(entry, "align_x", toggle = 1)
|
|
row.prop(entry, "align_y", toggle = 1)
|
|
row.prop(entry, "align_z", toggle = 1)
|
|
|
|
# show mode
|
|
col.separator()
|
|
col.label(text = 'Current Object (Active Object)')
|
|
col.prop(entry, "current_align_mode", expand = True)
|
|
col.label(text = 'Target Objects (Selected Objects)')
|
|
col.prop(entry, "target_align_mode", expand = True)
|
|
|
|
# show apply button
|
|
col.separator()
|
|
conditional_disable_area = col.column()
|
|
# only allow Apply when there is a selected axis
|
|
conditional_disable_area.enabled = entry.align_x == True or entry.align_y == True or entry.align_z == True
|
|
# show apply and counter
|
|
conditional_disable_area.prop(self, 'apply_flag', text = 'Apply', icon = 'CHECKMARK', toggle = 1)
|
|
conditional_disable_area.label(text = f'Total {len(self.align_history) - 1} applied alignments')
|
|
|
|
#region Core Functions
|
|
|
|
def _check_align_requirement() -> bool:
|
|
# check current obj
|
|
if bpy.context.active_object is None:
|
|
return False
|
|
|
|
# check target obj with filter of current obj
|
|
length = len(bpy.context.selected_objects)
|
|
if bpy.context.active_object in bpy.context.selected_objects:
|
|
length -= 1
|
|
return length != 0
|
|
|
|
def _prepare_objects() -> tuple[bpy.types.Object, set[bpy.types.Object]]:
|
|
# get current object
|
|
current_obj: bpy.types.Object = bpy.context.active_object
|
|
|
|
# get target objects
|
|
target_objs: set[bpy.types.Object] = set(bpy.context.selected_objects)
|
|
# remove active one
|
|
if current_obj in target_objs:
|
|
target_objs.remove(current_obj)
|
|
|
|
# return value
|
|
return (current_obj, target_objs)
|
|
|
|
def _align_objects(
|
|
current_obj: bpy.types.Object, target_objs: set[bpy.types.Object],
|
|
align_x: bool, align_y: bool, align_z: bool, current_mode: AlignMode, target_mode: AlignMode) -> None:
|
|
# if no align, skip
|
|
if not (align_x or align_y or align_z):
|
|
return
|
|
|
|
# calc current object data
|
|
current_obj_bbox: tuple[mathutils.Vector] = tuple(current_obj.matrix_world @ mathutils.Vector(corner) for corner in current_obj.bound_box)
|
|
current_obj_ref: mathutils.Vector = _get_object_ref_point(current_obj, current_obj_bbox, current_mode)
|
|
|
|
# process each target obj
|
|
for target_obj in target_objs:
|
|
# calc target object data
|
|
target_obj_bbox: tuple[mathutils.Vector] = tuple(target_obj.matrix_world @ mathutils.Vector(corner) for corner in target_obj.bound_box)
|
|
target_obj_ref: mathutils.Vector = _get_object_ref_point(target_obj, target_obj_bbox, target_mode)
|
|
# do align
|
|
if align_x:
|
|
target_obj.location.x += current_obj_ref.x - target_obj_ref.x
|
|
if align_y:
|
|
target_obj.location.y += current_obj_ref.y - target_obj_ref.y
|
|
if align_z:
|
|
target_obj.location.z += current_obj_ref.z - target_obj_ref.z
|
|
|
|
def _get_object_ref_point(obj: bpy.types.Object, corners: tuple[mathutils.Vector], mode: AlignMode) -> mathutils.Vector:
|
|
ref_pos: mathutils.Vector = mathutils.Vector((0, 0, 0))
|
|
|
|
match(mode):
|
|
case AlignMode.Min:
|
|
ref_pos.x = min((vec.x for vec in corners))
|
|
ref_pos.y = min((vec.y for vec in corners))
|
|
ref_pos.z = min((vec.z for vec in corners))
|
|
case AlignMode.Max:
|
|
ref_pos.x = max((vec.x for vec in corners))
|
|
ref_pos.y = max((vec.y for vec in corners))
|
|
ref_pos.z = max((vec.z for vec in corners))
|
|
case AlignMode.BBoxCenter:
|
|
max_vec_cache: mathutils.Vector = mathutils.Vector((0, 0, 0))
|
|
min_vec_cache: mathutils.Vector = mathutils.Vector((0, 0, 0))
|
|
|
|
min_vec_cache.x = min((vec.x for vec in corners))
|
|
min_vec_cache.y = min((vec.y for vec in corners))
|
|
min_vec_cache.z = min((vec.z for vec in corners))
|
|
max_vec_cache.x = max((vec.x for vec in corners))
|
|
max_vec_cache.y = max((vec.y for vec in corners))
|
|
max_vec_cache.z = max((vec.z for vec in corners))
|
|
|
|
ref_pos.x = (max_vec_cache.x + min_vec_cache.x) / 2
|
|
ref_pos.y = (max_vec_cache.y + min_vec_cache.y) / 2
|
|
ref_pos.z = (max_vec_cache.z + min_vec_cache.z) / 2
|
|
case AlignMode.AxisCenter:
|
|
ref_pos.x = obj.location.x
|
|
ref_pos.y = obj.location.y
|
|
ref_pos.z = obj.location.z
|
|
case _:
|
|
raise UTIL_functions.BBPException('inpossible align mode.')
|
|
|
|
return ref_pos
|
|
|
|
#endregion
|
|
|
|
def register() -> None:
|
|
bpy.utils.register_class(BBP_PG_legacy_align_history)
|
|
bpy.utils.register_class(BBP_OT_legacy_align)
|
|
|
|
def unregister() -> None:
|
|
bpy.utils.unregister_class(BBP_OT_legacy_align)
|
|
bpy.utils.unregister_class(BBP_PG_legacy_align_history)
|