From 9315ff723d2550bd1a731cef59e09c8340dab1e7 Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Tue, 26 Dec 2023 22:23:56 +0800 Subject: [PATCH] fix issues - apply transform when creating BME struct's vertices and faces. - add 3ds max align operator. support Apply button in a nasty way. --- bbp_ng/OP_ADDS_bme.py | 2 +- bbp_ng/OP_OBJECT_3dsmax_align.py | 0 bbp_ng/OP_OBJECT_legacy_align.py | 255 +++++++++++++++++++++++++++++++ bbp_ng/UTIL_bme.py | 34 ++++- bbp_ng/UTIL_virtools_types.py | 8 - bbp_ng/__init__.py | 8 +- 6 files changed, 292 insertions(+), 15 deletions(-) delete mode 100644 bbp_ng/OP_OBJECT_3dsmax_align.py create mode 100644 bbp_ng/OP_OBJECT_legacy_align.py diff --git a/bbp_ng/OP_ADDS_bme.py b/bbp_ng/OP_ADDS_bme.py index 83778ec..5f7186d 100644 --- a/bbp_ng/OP_ADDS_bme.py +++ b/bbp_ng/OP_ADDS_bme.py @@ -47,7 +47,7 @@ class BBP_OT_add_bme_struct(bpy.types.Operator): # In this internal cache list updator, check "outdated" flag first, if cache is outdated, update and reset flag. # Otherwise do nothing. # - # Reference: https://docs.blender.org/api/current/bpy.props.html#update-example + # Reference: https://docs.blender.org/api/current/bpy.props.html#update-example ## Compromise used "outdated" flag. outdated_flag: bpy.props.BoolProperty( diff --git a/bbp_ng/OP_OBJECT_3dsmax_align.py b/bbp_ng/OP_OBJECT_3dsmax_align.py deleted file mode 100644 index e69de29..0000000 diff --git a/bbp_ng/OP_OBJECT_legacy_align.py b/bbp_ng/OP_OBJECT_legacy_align.py new file mode 100644 index 0000000..7ab99b0 --- /dev/null +++ b/bbp_ng/OP_OBJECT_legacy_align.py @@ -0,0 +1,255 @@ +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 + + # add a new entry in history + self.align_history.add() + + # 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") + 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 (Other Objects)') + col.prop(entry, "target_align_mode", expand = True) + + # show apply button + col.separator() + col.prop(self, 'apply_flag', text = 'Apply', icon = 'CHECKMARK', toggle = 1) + +#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(): + bpy.utils.register_class(BBP_PG_legacy_align_history) + bpy.utils.register_class(BBP_OT_legacy_align) + +def unregister(): + bpy.utils.unregister_class(BBP_OT_legacy_align) + bpy.utils.unregister_class(BBP_PG_legacy_align_history) diff --git a/bbp_ng/UTIL_bme.py b/bbp_ng/UTIL_bme.py index ae754f1..7e62a12 100644 --- a/bbp_ng/UTIL_bme.py +++ b/bbp_ng/UTIL_bme.py @@ -33,6 +33,8 @@ TOKEN_SHOWCASE_CFGS_TITLE: str = 'title' TOKEN_SHOWCASE_CFGS_DESC: str = 'desc' TOKEN_SHOWCASE_CFGS_DEFAULT: str = 'default' +TOKEN_SKIP: str = 'skip' + TOKEN_PARAMS: str = 'params' TOKEN_PARAMS_FIELD: str = 'field' TOKEN_PARAMS_DATA: str = 'data' @@ -127,6 +129,9 @@ def _eval_showcase_cfgs_default(strl: str) -> typing.Any: def _eval_params(strl: str, cfgs_data: dict[str, typing.Any]) -> typing.Any: return eval(strl, _g_ProgFieldGlobals, cfgs_data) +def _eval_skip(strl: str, params_data: dict[str, typing.Any]) -> typing.Any: + return eval(strl, _g_ProgFieldGlobals, params_data) + def _eval_vars(strl: str, params_data: dict[str, typing.Any]) -> typing.Any: return eval(strl, _g_ProgFieldGlobals, params_data) @@ -272,6 +277,10 @@ def create_bme_struct( # get prototype first proto: dict[str, typing.Any] = _get_prototype_by_identifier(ident) + # check whether skip the whole struct before cal vars + if _eval_skip(proto[TOKEN_SKIP], params) == True: + return + # calc vars by given params # please note i will add entries directly into params dict # but the params dict will not used independently later, @@ -307,20 +316,34 @@ def create_bme_struct( # prepare mesh part data mesh_part: UTIL_blender_mesh.MeshWriterIngredient = UTIL_blender_mesh.MeshWriterIngredient() def vpos_iterator() -> typing.Iterator[UTIL_virtools_types.VxVector3]: + bv: mathutils.Vector = mathutils.Vector((0, 0, 0)) v: UTIL_virtools_types.VxVector3 = UTIL_virtools_types.VxVector3() for vec_idx in valid_vec_idx: # BME no need to convert co system - v.x, v.y, v.z = _eval_others(proto[TOKEN_VERTICES][vec_idx][TOKEN_VERTICES_DATA], params) + # but it need mul with transform matrix + bv.x, bv.y, bv.z = _eval_others(proto[TOKEN_VERTICES][vec_idx][TOKEN_VERTICES_DATA], params) + bv = transform @ bv + # yield result + v.x, v.y, v.z = bv.x, bv.y, bv.z yield v mesh_part.mVertexPosition = vpos_iterator() def vnml_iterator() -> typing.Iterator[UTIL_virtools_types.VxVector3]: + # calc normal used transform first + # ref: https://zhuanlan.zhihu.com/p/96717729 + nml_transform: mathutils.Matrix = transform.inverted_safe().transposed() + # prepare vars + bv: mathutils.Vector = mathutils.Vector((0, 0, 0)) v: UTIL_virtools_types.VxVector3 = UTIL_virtools_types.VxVector3() for face_idx in valid_face_idx: face_data: dict[str, typing.Any] = proto[TOKEN_FACES][face_idx] for i in range(len(face_data[TOKEN_FACES_INDICES])): - v.x, v.y, v.z = _eval_others(face_data[TOKEN_FACES_NORMALS][i], params) - # BME normals need normalize - UTIL_virtools_types.vxvector3_normalize(v) + # BME normals need transform by matrix first, + bv.x, bv.y, bv.z = _eval_others(face_data[TOKEN_FACES_NORMALS][i], params) + bv = nml_transform @ bv + # then normalize it + bv.normalize() + # yield result + v.x, v.y, v.z = bv.x, bv.y, bv.z yield v mesh_part.mVertexNormal = vnml_iterator() def vuv_iterator() -> typing.Iterator[UTIL_virtools_types.VxVector2]: @@ -328,6 +351,7 @@ def create_bme_struct( for face_idx in valid_face_idx: face_data: dict[str, typing.Any] = proto[TOKEN_FACES][face_idx] for i in range(len(face_data[TOKEN_FACES_INDICES])): + # BME uv do not need any extra process v.x, v.y = _eval_others(face_data[TOKEN_FACES_UVS][i], params) yield v mesh_part.mVertexUV = vuv_iterator() @@ -390,7 +414,7 @@ def create_bme_struct( proto_instance[TOKEN_INSTANCES_IDENTIFIER], writer, bmemtl, - _eval_others(proto_instance[TOKEN_INSTANCES_TRANSFORM], params), + transform @ _eval_others(proto_instance[TOKEN_INSTANCES_TRANSFORM], params), instance_params ) diff --git a/bbp_ng/UTIL_virtools_types.py b/bbp_ng/UTIL_virtools_types.py index 4a41bef..6dd045f 100644 --- a/bbp_ng/UTIL_virtools_types.py +++ b/bbp_ng/UTIL_virtools_types.py @@ -27,14 +27,6 @@ def vxvector3_conv_co(self: VxVector3) -> None: """ self.y, self.z = self.z, self.y -def vxvector3_normalize(self: VxVector3) -> None: - """ - Normalize given Vector - """ - cache: mathutils.Vector = mathutils.Vector((self.x, self.y, self.z)) - cache.normalize() - self.x, self.y, self.z = cache.x, cache.y, cache.z - #endregion #region VxMatrix Patch diff --git a/bbp_ng/__init__.py b/bbp_ng/__init__.py index c625da7..56c8578 100644 --- a/bbp_ng/__init__.py +++ b/bbp_ng/__init__.py @@ -33,13 +33,14 @@ from . import PROP_preferences, PROP_ptrprop_resolver, PROP_virtools_material, P from . import OP_IMPORT_bmfile, OP_EXPORT_bmfile, OP_IMPORT_virtools, OP_EXPORT_virtools from . import OP_UV_flatten_uv, OP_UV_rail_uv from . import OP_ADDS_component, OP_ADDS_bme +from . import OP_OBJECT_legacy_align #region Menu # ===== Menu Defines ===== class BBP_MT_View3DMenu(bpy.types.Menu): - """Ballance 3D operators""" + """Ballance 3D Operators""" bl_idname = "BBP_MT_View3DMenu" bl_label = "Ballance" @@ -47,6 +48,7 @@ class BBP_MT_View3DMenu(bpy.types.Menu): layout = self.layout layout.operator(OP_UV_flatten_uv.BBP_OT_flatten_uv.bl_idname) layout.operator(OP_UV_rail_uv.BBP_OT_rail_uv.bl_idname) + layout.operator(OP_OBJECT_legacy_align.BBP_OT_legacy_align.bl_idname) class BBP_MT_AddBmeMenu(bpy.types.Menu): """Add Ballance Floor""" @@ -163,6 +165,8 @@ def register() -> None: OP_ADDS_component.register() OP_ADDS_bme.register() + OP_OBJECT_legacy_align.register() + # register other classes for cls in g_BldClasses: bpy.utils.register_class(cls) @@ -184,6 +188,8 @@ def unregister() -> None: bpy.utils.unregister_class(cls) # unregister modules + OP_OBJECT_legacy_align.unregister() + OP_ADDS_bme.unregister() OP_ADDS_component.unregister() OP_UV_flatten_uv.unregister()