fix issues

- apply transform when creating BME struct's vertices and faces.
- add 3ds max align operator. support Apply button in a nasty way.
This commit is contained in:
yyc12345 2023-12-26 22:23:56 +08:00
parent 50b7eb0bce
commit 9315ff723d
6 changed files with 292 additions and 15 deletions

View File

@ -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. # In this internal cache list updator, check "outdated" flag first, if cache is outdated, update and reset flag.
# Otherwise do nothing. # 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. ## Compromise used "outdated" flag.
outdated_flag: bpy.props.BoolProperty( outdated_flag: bpy.props.BoolProperty(

View File

@ -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)

View File

@ -33,6 +33,8 @@ TOKEN_SHOWCASE_CFGS_TITLE: str = 'title'
TOKEN_SHOWCASE_CFGS_DESC: str = 'desc' TOKEN_SHOWCASE_CFGS_DESC: str = 'desc'
TOKEN_SHOWCASE_CFGS_DEFAULT: str = 'default' TOKEN_SHOWCASE_CFGS_DEFAULT: str = 'default'
TOKEN_SKIP: str = 'skip'
TOKEN_PARAMS: str = 'params' TOKEN_PARAMS: str = 'params'
TOKEN_PARAMS_FIELD: str = 'field' TOKEN_PARAMS_FIELD: str = 'field'
TOKEN_PARAMS_DATA: str = 'data' 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: def _eval_params(strl: str, cfgs_data: dict[str, typing.Any]) -> typing.Any:
return eval(strl, _g_ProgFieldGlobals, cfgs_data) 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: def _eval_vars(strl: str, params_data: dict[str, typing.Any]) -> typing.Any:
return eval(strl, _g_ProgFieldGlobals, params_data) return eval(strl, _g_ProgFieldGlobals, params_data)
@ -272,6 +277,10 @@ def create_bme_struct(
# get prototype first # get prototype first
proto: dict[str, typing.Any] = _get_prototype_by_identifier(ident) 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 # calc vars by given params
# please note i will add entries directly into params dict # please note i will add entries directly into params dict
# but the params dict will not used independently later, # but the params dict will not used independently later,
@ -307,20 +316,34 @@ def create_bme_struct(
# prepare mesh part data # prepare mesh part data
mesh_part: UTIL_blender_mesh.MeshWriterIngredient = UTIL_blender_mesh.MeshWriterIngredient() mesh_part: UTIL_blender_mesh.MeshWriterIngredient = UTIL_blender_mesh.MeshWriterIngredient()
def vpos_iterator() -> typing.Iterator[UTIL_virtools_types.VxVector3]: 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() v: UTIL_virtools_types.VxVector3 = UTIL_virtools_types.VxVector3()
for vec_idx in valid_vec_idx: for vec_idx in valid_vec_idx:
# BME no need to convert co system # 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 yield v
mesh_part.mVertexPosition = vpos_iterator() mesh_part.mVertexPosition = vpos_iterator()
def vnml_iterator() -> typing.Iterator[UTIL_virtools_types.VxVector3]: 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() v: UTIL_virtools_types.VxVector3 = UTIL_virtools_types.VxVector3()
for face_idx in valid_face_idx: for face_idx in valid_face_idx:
face_data: dict[str, typing.Any] = proto[TOKEN_FACES][face_idx] face_data: dict[str, typing.Any] = proto[TOKEN_FACES][face_idx]
for i in range(len(face_data[TOKEN_FACES_INDICES])): 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 transform by matrix first,
# BME normals need normalize bv.x, bv.y, bv.z = _eval_others(face_data[TOKEN_FACES_NORMALS][i], params)
UTIL_virtools_types.vxvector3_normalize(v) 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 yield v
mesh_part.mVertexNormal = vnml_iterator() mesh_part.mVertexNormal = vnml_iterator()
def vuv_iterator() -> typing.Iterator[UTIL_virtools_types.VxVector2]: def vuv_iterator() -> typing.Iterator[UTIL_virtools_types.VxVector2]:
@ -328,6 +351,7 @@ def create_bme_struct(
for face_idx in valid_face_idx: for face_idx in valid_face_idx:
face_data: dict[str, typing.Any] = proto[TOKEN_FACES][face_idx] face_data: dict[str, typing.Any] = proto[TOKEN_FACES][face_idx]
for i in range(len(face_data[TOKEN_FACES_INDICES])): 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) v.x, v.y = _eval_others(face_data[TOKEN_FACES_UVS][i], params)
yield v yield v
mesh_part.mVertexUV = vuv_iterator() mesh_part.mVertexUV = vuv_iterator()
@ -390,7 +414,7 @@ def create_bme_struct(
proto_instance[TOKEN_INSTANCES_IDENTIFIER], proto_instance[TOKEN_INSTANCES_IDENTIFIER],
writer, writer,
bmemtl, bmemtl,
_eval_others(proto_instance[TOKEN_INSTANCES_TRANSFORM], params), transform @ _eval_others(proto_instance[TOKEN_INSTANCES_TRANSFORM], params),
instance_params instance_params
) )

View File

@ -27,14 +27,6 @@ def vxvector3_conv_co(self: VxVector3) -> None:
""" """
self.y, self.z = self.z, self.y 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 #endregion
#region VxMatrix Patch #region VxMatrix Patch

View File

@ -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_IMPORT_bmfile, OP_EXPORT_bmfile, OP_IMPORT_virtools, OP_EXPORT_virtools
from . import OP_UV_flatten_uv, OP_UV_rail_uv from . import OP_UV_flatten_uv, OP_UV_rail_uv
from . import OP_ADDS_component, OP_ADDS_bme from . import OP_ADDS_component, OP_ADDS_bme
from . import OP_OBJECT_legacy_align
#region Menu #region Menu
# ===== Menu Defines ===== # ===== Menu Defines =====
class BBP_MT_View3DMenu(bpy.types.Menu): class BBP_MT_View3DMenu(bpy.types.Menu):
"""Ballance 3D operators""" """Ballance 3D Operators"""
bl_idname = "BBP_MT_View3DMenu" bl_idname = "BBP_MT_View3DMenu"
bl_label = "Ballance" bl_label = "Ballance"
@ -47,6 +48,7 @@ class BBP_MT_View3DMenu(bpy.types.Menu):
layout = self.layout layout = self.layout
layout.operator(OP_UV_flatten_uv.BBP_OT_flatten_uv.bl_idname) 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_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): class BBP_MT_AddBmeMenu(bpy.types.Menu):
"""Add Ballance Floor""" """Add Ballance Floor"""
@ -163,6 +165,8 @@ def register() -> None:
OP_ADDS_component.register() OP_ADDS_component.register()
OP_ADDS_bme.register() OP_ADDS_bme.register()
OP_OBJECT_legacy_align.register()
# register other classes # register other classes
for cls in g_BldClasses: for cls in g_BldClasses:
bpy.utils.register_class(cls) bpy.utils.register_class(cls)
@ -184,6 +188,8 @@ def unregister() -> None:
bpy.utils.unregister_class(cls) bpy.utils.unregister_class(cls)
# unregister modules # unregister modules
OP_OBJECT_legacy_align.unregister()
OP_ADDS_bme.unregister() OP_ADDS_bme.unregister()
OP_ADDS_component.unregister() OP_ADDS_component.unregister()
OP_UV_flatten_uv.unregister() OP_UV_flatten_uv.unregister()