Compare commits
7 Commits
ab266a07fb
...
7f33e4ad92
Author | SHA1 | Date | |
---|---|---|---|
7f33e4ad92 | |||
a2b8f41a21 | |||
93f23abeb9 | |||
4ba3ff9e5a | |||
a9a889a8fd | |||
fc34b19a42 | |||
9e65d258d7 |
@ -5,7 +5,7 @@ from . import UTIL_functions, UTIL_translation, UTIL_bme
|
||||
|
||||
#region BME Adder
|
||||
|
||||
_g_EnumHelper_BmeStructType: UTIL_bme.EnumPropHelper = UTIL_bme.EnumPropHelper()
|
||||
_g_EnumHelper_BmeStructType = UTIL_bme.EnumPropHelper()
|
||||
|
||||
class BBP_PG_bme_adder_cfgs(bpy.types.PropertyGroup):
|
||||
prop_int: bpy.props.IntProperty(
|
||||
@ -37,39 +37,44 @@ class BBP_OT_add_bme_struct(bpy.types.Operator):
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
bl_translation_context = 'BBP_OT_add_bme_struct'
|
||||
|
||||
## There is a compromise due to the shitty Blender design.
|
||||
# YYC MARK:
|
||||
# ===== 20231217 =====
|
||||
# There is a compromise due to the shitty Blender design.
|
||||
# The passed `self` of Blender Property update function is not the instance of operator,
|
||||
# but a simple OperatorProperties.
|
||||
# It mean that I can not visit the full operator, only what I can do is visit existing
|
||||
# Blender properties.
|
||||
#
|
||||
# The passed `self` of Blender Property update function is not the instance of operator,
|
||||
# but a simple OperatorProperties.
|
||||
# It mean that I can not visit the full operator, only what I can do is visit existing
|
||||
# Blender properties.
|
||||
# So these is the solution about generating cache list according to the change of bme struct type.
|
||||
# First, update function will only set a "outdated" flag for operator which is a pre-registered Blender property.
|
||||
# The "outdated" flags is not showen and not saved.
|
||||
# Then call a internal cache list update function at the begin of `invoke`, `execute` and `draw`.
|
||||
# In this internal cache list updator, check "outdated" flag first, if cache is outdated, update and reset flag.
|
||||
# Otherwise do nothing.
|
||||
#
|
||||
# So these is the solution about generating cache list according to the change of bme struct type.
|
||||
# First, update function will only set a "outdated" flag for operator which is a pre-registered Blender property.
|
||||
# The "outdated" flags is not showen and not saved.
|
||||
# Then call a internal cache list update function at the begin of `invoke`, `execute` and `draw`.
|
||||
# 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(
|
||||
# TR: Property not showen should not have name and desc.
|
||||
# name = "Outdated Type",
|
||||
# description = "Internal flag.",
|
||||
options = {'HIDDEN', 'SKIP_SAVE'},
|
||||
default = False
|
||||
) # type: ignore
|
||||
# ===== 20250131 =====
|
||||
# There is a fatal performance bug when I adding BME operator list into 3D View sidebar panels (N Menu).
|
||||
# It will cause calling my Panel's `draw` function infinityly of Panel in each render tick,
|
||||
# which calls `BBP_OT_add_bme_struct.draw_blc_menu` directly,
|
||||
# eat too much CPU and GPU resources and make the whole Blender be laggy.
|
||||
#
|
||||
# After some research, I found that if I comment the parameter `update` of the member `bme_struct_type`,
|
||||
# everything will be resolved.
|
||||
# It even doesn't work that do nothing in update function.
|
||||
# So I realize that sidebar panel may not be compatible with update function.
|
||||
# After reading the note written above, I decide to remove the whole feature of this ugly implementation,
|
||||
# so that I need to remove the ability that changing BME prototype type in left-bottom window.
|
||||
#
|
||||
# After talking with the requestor of this feature, ZZQ,
|
||||
# he agree with my decision and I think this change will not broke any experience of BBP.
|
||||
|
||||
## A BME struct cfgs descriptor cache list
|
||||
# Not only the descriptor self, also the cfg associated index in bme_struct_cfgs
|
||||
bme_struct_cfg_index_cache: list[tuple[UTIL_bme.PrototypeShowcaseCfgDescriptor, int]]
|
||||
|
||||
def __internal_update_bme_struct_type(self) -> None:
|
||||
# if not outdated, skip
|
||||
if not self.outdated_flag: return
|
||||
|
||||
def __build_bme_struct_cfg_index_cache(self) -> None:
|
||||
# get available cfg entires
|
||||
cfgs: typing.Iterator[UTIL_bme.PrototypeShowcaseCfgDescriptor]
|
||||
cfgs = _g_EnumHelper_BmeStructType.get_bme_showcase_cfgs(
|
||||
@ -125,21 +130,10 @@ class BBP_OT_add_bme_struct(bpy.types.Operator):
|
||||
for i in range(6):
|
||||
op_cfgs_visitor[cfg_index + i].prop_bool = default_values[i]
|
||||
|
||||
# reset outdated flag
|
||||
self.outdated_flag = False
|
||||
|
||||
# the updator for default side value
|
||||
def bme_struct_type_updated(self, context):
|
||||
# update outdated flag
|
||||
self.outdated_flag = True
|
||||
# blender required
|
||||
return None
|
||||
|
||||
bme_struct_type: bpy.props.EnumProperty(
|
||||
name = "Type",
|
||||
description = "The type of BME structure.",
|
||||
items = _g_EnumHelper_BmeStructType.generate_items(),
|
||||
update = bme_struct_type_updated,
|
||||
translation_context = 'BBP_OT_add_bme_struct/property'
|
||||
) # type: ignore
|
||||
|
||||
@ -180,19 +174,16 @@ class BBP_OT_add_bme_struct(bpy.types.Operator):
|
||||
self.extra_translation = (0.0, 0.0, 0.0)
|
||||
self.extra_rotation = (0.0, 0.0, 0.0)
|
||||
self.extra_scale = (1.0, 1.0, 1.0)
|
||||
|
||||
# create internal list
|
||||
self.bme_struct_cfg_index_cache = []
|
||||
# trigger default bme struct type updator
|
||||
self.bme_struct_type_updated(context)
|
||||
# call internal updator
|
||||
self.__internal_update_bme_struct_type()
|
||||
# call internal builder to load prototype data inside it
|
||||
self.__build_bme_struct_cfg_index_cache()
|
||||
|
||||
# run execute() function
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
# call internal updator
|
||||
self.__internal_update_bme_struct_type()
|
||||
|
||||
# create cfg visitor
|
||||
op_cfgs_visitor: UTIL_functions.CollectionVisitor[BBP_PG_bme_adder_cfgs]
|
||||
op_cfgs_visitor = UTIL_functions.CollectionVisitor(self.bme_struct_cfgs)
|
||||
@ -231,13 +222,8 @@ class BBP_OT_add_bme_struct(bpy.types.Operator):
|
||||
return {'FINISHED'}
|
||||
|
||||
def draw(self, context):
|
||||
# call internal updator
|
||||
self.__internal_update_bme_struct_type()
|
||||
|
||||
# start drawing
|
||||
layout: bpy.types.UILayout = self.layout
|
||||
# show type
|
||||
layout.prop(self, 'bme_struct_type')
|
||||
|
||||
# create cfg visitor
|
||||
op_cfgs_visitor: UTIL_functions.CollectionVisitor[BBP_PG_bme_adder_cfgs]
|
||||
@ -300,7 +286,7 @@ class BBP_OT_add_bme_struct(bpy.types.Operator):
|
||||
cls.bl_idname,
|
||||
text = _g_EnumHelper_BmeStructType.get_bme_showcase_title(ident),
|
||||
icon_value = _g_EnumHelper_BmeStructType.get_bme_showcase_icon(ident),
|
||||
text_ctxt = UTIL_translation.build_prototype_showcase_context(ident)
|
||||
text_ctxt = UTIL_translation.build_prototype_showcase_context(ident),
|
||||
)
|
||||
# and assign its init type value
|
||||
cop.bme_struct_type = _g_EnumHelper_BmeStructType.to_selection(ident)
|
||||
|
@ -176,7 +176,7 @@ class _GeneralComponentCreator():
|
||||
|
||||
#endregion
|
||||
|
||||
#region Noemal Component Adder
|
||||
#region Normal Component Adder
|
||||
|
||||
# element enum prop helper
|
||||
|
||||
@ -184,7 +184,7 @@ def _get_component_icon_by_name(elename: str):
|
||||
icon: int | None = UTIL_icons_manager.get_component_icon(elename)
|
||||
if icon is None: return UTIL_icons_manager.get_empty_icon()
|
||||
else: return icon
|
||||
_g_EnumHelper_Component: UTIL_functions.EnumPropHelper = UTIL_functions.EnumPropHelper(
|
||||
_g_EnumHelper_Component = UTIL_functions.EnumPropHelper(
|
||||
PROP_ballance_element.BallanceElementType,
|
||||
lambda x: str(x.value),
|
||||
lambda x: PROP_ballance_element.BallanceElementType(int(x)),
|
||||
@ -217,7 +217,7 @@ class BBP_OT_add_component(bpy.types.Operator, ComponentSectorParam):
|
||||
layout.prop(self, "component_type")
|
||||
|
||||
# only show sector for non-PE/PS component
|
||||
eletype: PROP_ballance_element.BallanceElementType = _g_EnumHelper_Component.get_selection(self.component_type)
|
||||
eletype = _g_EnumHelper_Component.get_selection(self.component_type)
|
||||
if eletype != PROP_ballance_element.BallanceElementType.PS_FourFlames and eletype != PROP_ballance_element.BallanceElementType.PE_Balloon:
|
||||
self.draw_component_sector_params(layout)
|
||||
|
||||
|
@ -9,19 +9,35 @@ class AlignMode(enum.IntEnum):
|
||||
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_AlignModeDesc: dict[AlignMode, tuple[str, str, str]] = {
|
||||
AlignMode.Min: ("Min", "The min value in specified axis.", "REMOVE"),
|
||||
AlignMode.BBoxCenter: ("Center (Bounding Box)", "The bounding box center in specified axis.", "SHADING_BBOX"),
|
||||
AlignMode.AxisCenter: ("Center (Axis)", "The object's source point in specified axis.", "OBJECT_ORIGIN"),
|
||||
AlignMode.Max: ("Max", "The max value in specified axis.", "ADD"),
|
||||
}
|
||||
_g_EnumHelper_AlignMode: UTIL_functions.EnumPropHelper = UTIL_functions.EnumPropHelper(
|
||||
_g_EnumHelper_AlignMode = 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 _: ''
|
||||
lambda x: _g_AlignModeDesc[x][2]
|
||||
)
|
||||
|
||||
class CurrentInstance(enum.IntEnum):
|
||||
ActiveObject = enum.auto()
|
||||
Cursor = enum.auto()
|
||||
_g_CurrentInstanceDesc: dict[CurrentInstance, tuple[str, str, str]] = {
|
||||
CurrentInstance.ActiveObject: ("Active Object", "Use Active Object as Current Object", "OBJECT_DATA"),
|
||||
CurrentInstance.Cursor: ("3D Cursor", "Use 3D Cursor as Current Object", "CURSOR"),
|
||||
}
|
||||
_g_EnumHelper_CurrentInstance = UTIL_functions.EnumPropHelper(
|
||||
CurrentInstance,
|
||||
lambda x: str(x.value),
|
||||
lambda x: CurrentInstance(int(x)),
|
||||
lambda x: _g_CurrentInstanceDesc[x][0],
|
||||
lambda x: _g_CurrentInstanceDesc[x][1],
|
||||
lambda x: _g_CurrentInstanceDesc[x][2]
|
||||
)
|
||||
|
||||
#endregion
|
||||
@ -55,14 +71,23 @@ class BBP_PG_legacy_align_history(bpy.types.PropertyGroup):
|
||||
default = False,
|
||||
translation_context = 'BBP_PG_legacy_align_history/property'
|
||||
) # type: ignore
|
||||
current_instance: bpy.props.EnumProperty(
|
||||
name = "Current Instance",
|
||||
description = "Decide which instance should be used as Current Object",
|
||||
items = _g_EnumHelper_CurrentInstance.generate_items(),
|
||||
default = _g_EnumHelper_CurrentInstance.to_selection(CurrentInstance.ActiveObject),
|
||||
translation_context = 'BBP_PG_legacy_align_history/property'
|
||||
) # type: ignore
|
||||
current_align_mode: bpy.props.EnumProperty(
|
||||
name = "Current Object (Active Object)",
|
||||
name = "Current Object",
|
||||
description = "The align mode applied to Current Object",
|
||||
items = _g_EnumHelper_AlignMode.generate_items(),
|
||||
default = _g_EnumHelper_AlignMode.to_selection(AlignMode.AxisCenter),
|
||||
translation_context = 'BBP_PG_legacy_align_history/property'
|
||||
) # type: ignore
|
||||
target_align_mode: bpy.props.EnumProperty(
|
||||
name = "Target Objects (Selected Objects)",
|
||||
name = "Target Objects",
|
||||
description = "The align mode applied to Target Objects (selected objects except active object if Current Instance is active object)",
|
||||
items = _g_EnumHelper_AlignMode.generate_items(),
|
||||
default = _g_EnumHelper_AlignMode.to_selection(AlignMode.AxisCenter),
|
||||
translation_context = 'BBP_PG_legacy_align_history/property'
|
||||
@ -148,7 +173,7 @@ class BBP_OT_legacy_align(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
# get processed objects
|
||||
(current_obj, target_objs) = _prepare_objects()
|
||||
(current_obj, current_cursor, target_objs) = _prepare_objects()
|
||||
# YYC MARK:
|
||||
# This statement is VERY IMPORTANT.
|
||||
# If this statement is not presented, Blender will return identity matrix
|
||||
@ -162,7 +187,8 @@ class BBP_OT_legacy_align(bpy.types.Operator):
|
||||
histories = UTIL_functions.CollectionVisitor(self.align_history)
|
||||
for entry in histories:
|
||||
_align_objects(
|
||||
current_obj, target_objs,
|
||||
_g_EnumHelper_CurrentInstance.get_selection(entry.current_instance),
|
||||
current_obj, current_cursor, 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)
|
||||
@ -185,10 +211,21 @@ class BBP_OT_legacy_align(bpy.types.Operator):
|
||||
row.prop(entry, "align_y", toggle = 1)
|
||||
row.prop(entry, "align_z", toggle = 1)
|
||||
|
||||
# show mode
|
||||
# show current instance
|
||||
col.separator()
|
||||
col.label(text='Current Object (Active Object)', text_ctxt='BBP_OT_legacy_align/draw')
|
||||
col.prop(entry, "current_align_mode", expand = True)
|
||||
col.label(text='Current Instance', text_ctxt='BBP_OT_legacy_align/draw')
|
||||
# it should be shown in horizon so we create a new sublayout
|
||||
row = col.row()
|
||||
row.prop(entry, 'current_instance', expand=True)
|
||||
|
||||
# show instance and mode
|
||||
col.separator()
|
||||
# only show current object mode if current instance is active object,
|
||||
# because there is no mode for 3d cursor.
|
||||
current_instnce = _g_EnumHelper_CurrentInstance.get_selection(entry.current_instance)
|
||||
if current_instnce == CurrentInstance.ActiveObject:
|
||||
col.label(text='Current Object (Active Object)', text_ctxt='BBP_OT_legacy_align/draw')
|
||||
col.prop(entry, "current_align_mode", expand = True)
|
||||
col.label(text='Target Objects (Selected Objects)', text_ctxt='BBP_OT_legacy_align/draw')
|
||||
col.prop(entry, "target_align_mode", expand = True)
|
||||
|
||||
@ -206,44 +243,66 @@ class BBP_OT_legacy_align(bpy.types.Operator):
|
||||
#region Core Functions
|
||||
|
||||
def _check_align_requirement() -> bool:
|
||||
# if we are not in object mode, do not do legacy align
|
||||
# If we are not in object mode, do not do legacy align
|
||||
if not UTIL_functions.is_in_object_mode():
|
||||
return False
|
||||
|
||||
# check current obj
|
||||
# YYC MARK:
|
||||
# We still need to check active object (as current object)
|
||||
# although we can choose align with active object or 3d cursor.
|
||||
# Because we can not make any promise that user will
|
||||
# select Active Object or 3D Cursor as current object before executing this operator.
|
||||
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
|
||||
# YYC MARK:
|
||||
# Roughly check selected objects.
|
||||
# We do not need exclude active object from selected objects,
|
||||
# because active object may be moved when 3D Cursor is current object.
|
||||
if len(bpy.context.selected_objects) == 0:
|
||||
return False
|
||||
|
||||
# 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 True
|
||||
|
||||
def _prepare_objects() -> tuple[bpy.types.Object, mathutils.Vector, list[bpy.types.Object]]:
|
||||
# Fetch current object
|
||||
current_obj = typing.cast(bpy.types.Object, bpy.context.active_object)
|
||||
|
||||
# Fetch 3d cursor location
|
||||
current_cursor: mathutils.Vector = bpy.context.scene.cursor.location
|
||||
|
||||
# YYC MARK:
|
||||
# Fetch target objects and do NOT remove active object from it.
|
||||
# because active object will be moved when current instance is 3D Cursor.
|
||||
target_objs: list[bpy.types.Object] = bpy.context.selected_objects[:]
|
||||
|
||||
# return value
|
||||
return (current_obj, target_objs)
|
||||
return (current_obj, current_cursor, target_objs)
|
||||
|
||||
def _align_objects(
|
||||
current_obj: bpy.types.Object, target_objs: set[bpy.types.Object],
|
||||
current_instance: CurrentInstance,
|
||||
current_obj: bpy.types.Object, current_cursor: mathutils.Vector, target_objs: list[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_ref: mathutils.Vector = _get_object_ref_point(current_obj, current_mode)
|
||||
current_obj_ref: mathutils.Vector
|
||||
match current_instance:
|
||||
case CurrentInstance.ActiveObject:
|
||||
current_obj_ref = _get_object_ref_point(current_obj, current_mode)
|
||||
case CurrentInstance.Cursor:
|
||||
current_obj_ref = current_cursor
|
||||
|
||||
# process each target obj
|
||||
for target_obj in target_objs:
|
||||
# YYC MARK:
|
||||
# If we use active object as current instance, we need exclude it from target objects,
|
||||
# because there is no pre-exclude considering the scenario that 3D Cursor is current instance.
|
||||
if current_instance == CurrentInstance.ActiveObject and current_obj == target_obj:
|
||||
continue
|
||||
|
||||
# calc target object data
|
||||
target_obj_ref: mathutils.Vector = _get_object_ref_point(target_obj, target_mode)
|
||||
# build translation transform
|
||||
@ -256,21 +315,21 @@ def _align_objects(
|
||||
# apply translation transform to left side (add into original matrix)
|
||||
target_obj.matrix_world = target_obj_translation_matrix @ target_obj.matrix_world
|
||||
|
||||
bpy.context.scene.update_tag
|
||||
|
||||
def _get_object_ref_point(obj: bpy.types.Object, mode: AlignMode) -> mathutils.Vector:
|
||||
ref_pos: mathutils.Vector = mathutils.Vector((0, 0, 0))
|
||||
ref_pos = mathutils.Vector((0, 0, 0))
|
||||
|
||||
# calc bounding box data
|
||||
corners: tuple[mathutils.Vector] = tuple(obj.matrix_world @ mathutils.Vector(corner) for corner in obj.bound_box)
|
||||
bbox_min_corner: mathutils.Vector = mathutils.Vector((0, 0, 0))
|
||||
bbox_min_corner.x = min((vec.x for vec in corners))
|
||||
bbox_min_corner.y = min((vec.y for vec in corners))
|
||||
bbox_min_corner.z = min((vec.z for vec in corners))
|
||||
bbox_max_corner: mathutils.Vector = mathutils.Vector((0, 0, 0))
|
||||
bbox_max_corner.x = max((vec.x for vec in corners))
|
||||
bbox_max_corner.y = max((vec.y for vec in corners))
|
||||
bbox_max_corner.z = max((vec.z for vec in corners))
|
||||
corners: tuple[mathutils.Vector, ...] = tuple(obj.matrix_world @ mathutils.Vector(corner) for corner in obj.bound_box)
|
||||
bbox_min_corner = mathutils.Vector((
|
||||
min((vec.x for vec in corners)),
|
||||
min((vec.y for vec in corners)),
|
||||
min((vec.z for vec in corners)),
|
||||
))
|
||||
bbox_max_corner = mathutils.Vector((
|
||||
max((vec.x for vec in corners)),
|
||||
max((vec.y for vec in corners)),
|
||||
max((vec.z for vec in corners)),
|
||||
))
|
||||
|
||||
# return value by given align mode
|
||||
match(mode):
|
||||
|
@ -18,7 +18,7 @@ _g_SelectModeDesc: dict[SelectMode, tuple[str, str, str]] = {
|
||||
SelectMode.Difference: ('Invert', 'Inverts the selection.', 'SELECT_DIFFERENCE'),
|
||||
SelectMode.Intersect: ('Intersect', 'Selects items that intersect with the existing selection.', 'SELECT_INTERSECT')
|
||||
}
|
||||
_g_EnumHelper_SelectMode: UTIL_functions.EnumPropHelper = UTIL_functions.EnumPropHelper(
|
||||
_g_EnumHelper_SelectMode = UTIL_functions.EnumPropHelper(
|
||||
SelectMode,
|
||||
lambda x: str(x.value),
|
||||
lambda x: SelectMode(int(x)),
|
||||
|
@ -205,7 +205,7 @@ class VirtoolsGroupsPreset(enum.Enum):
|
||||
|
||||
Shadow = "Shadow"
|
||||
|
||||
_g_VtGrpPresetValues: tuple[str] = tuple(map(lambda x: x.value, VirtoolsGroupsPreset))
|
||||
_g_VtGrpPresetValues: tuple[str, ...] = tuple(map(lambda x: x.value, VirtoolsGroupsPreset))
|
||||
|
||||
## Some of group names are not matched with icon name
|
||||
# So we create a convertion map to convert them.
|
||||
@ -236,7 +236,7 @@ def _get_group_icon_by_name(gp_name: str) -> int:
|
||||
if value is not None: return value
|
||||
else: return UTIL_icons_manager.get_empty_icon()
|
||||
# blender group name prop helper
|
||||
_g_EnumHelper_Group: UTIL_functions.EnumPropHelper = UTIL_functions.EnumPropHelper(
|
||||
_g_EnumHelper_Group = UTIL_functions.EnumPropHelper(
|
||||
VirtoolsGroupsPreset,
|
||||
lambda x: x.value, # member is string self
|
||||
lambda x: VirtoolsGroupsPreset(x), # convert directly because it is StrEnum.
|
||||
|
@ -73,7 +73,7 @@ class RawVirtoolsLight():
|
||||
|
||||
# Blender Property Group
|
||||
|
||||
_g_Helper_VXLIGHT_TYPE: UTIL_virtools_types.EnumPropHelper = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXLIGHT_TYPE)
|
||||
_g_Helper_VXLIGHT_TYPE = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXLIGHT_TYPE)
|
||||
|
||||
class BBP_PG_virtools_light(bpy.types.PropertyGroup):
|
||||
light_type: bpy.props.EnumProperty(
|
||||
|
@ -114,13 +114,13 @@ class RawVirtoolsMaterial():
|
||||
|
||||
#region Blender Enum Prop Helper (Virtools type specified)
|
||||
|
||||
_g_Helper_VXTEXTURE_BLENDMODE: UTIL_virtools_types.EnumPropHelper = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXTEXTURE_BLENDMODE)
|
||||
_g_Helper_VXTEXTURE_FILTERMODE: UTIL_virtools_types.EnumPropHelper = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXTEXTURE_FILTERMODE)
|
||||
_g_Helper_VXTEXTURE_ADDRESSMODE: UTIL_virtools_types.EnumPropHelper = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXTEXTURE_ADDRESSMODE)
|
||||
_g_Helper_VXBLEND_MODE: UTIL_virtools_types.EnumPropHelper = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXBLEND_MODE)
|
||||
_g_Helper_VXFILL_MODE: UTIL_virtools_types.EnumPropHelper = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXFILL_MODE)
|
||||
_g_Helper_VXSHADE_MODE: UTIL_virtools_types.EnumPropHelper = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXSHADE_MODE)
|
||||
_g_Helper_VXCMPFUNC: UTIL_virtools_types.EnumPropHelper = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXCMPFUNC)
|
||||
_g_Helper_VXTEXTURE_BLENDMODE = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXTEXTURE_BLENDMODE)
|
||||
_g_Helper_VXTEXTURE_FILTERMODE = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXTEXTURE_FILTERMODE)
|
||||
_g_Helper_VXTEXTURE_ADDRESSMODE = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXTEXTURE_ADDRESSMODE)
|
||||
_g_Helper_VXBLEND_MODE = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXBLEND_MODE)
|
||||
_g_Helper_VXFILL_MODE = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXFILL_MODE)
|
||||
_g_Helper_VXSHADE_MODE = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXSHADE_MODE)
|
||||
_g_Helper_VXCMPFUNC = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXCMPFUNC)
|
||||
|
||||
#endregion
|
||||
|
||||
@ -558,7 +558,7 @@ def preset_virtools_material(mtl: bpy.types.Material, preset_type: MaterialPrese
|
||||
set_raw_virtools_material(mtl, preset_data)
|
||||
|
||||
# create preset enum blender helper
|
||||
_g_Helper_MtlPreset: UTIL_functions.EnumPropHelper = UTIL_functions.EnumPropHelper(
|
||||
_g_Helper_MtlPreset = UTIL_functions.EnumPropHelper(
|
||||
MaterialPresetType,
|
||||
lambda x: str(x.value),
|
||||
lambda x: MaterialPresetType(int(x)),
|
||||
@ -572,13 +572,13 @@ _g_Helper_MtlPreset: UTIL_functions.EnumPropHelper = UTIL_functions.EnumPropHelp
|
||||
#region Fix Material
|
||||
|
||||
def fix_material(mtl: bpy.types.Material) -> bool:
|
||||
"""!
|
||||
"""
|
||||
Fix single Blender material.
|
||||
|
||||
@remark The implementation of this function is copied from BallanceVirtoolsHelper/bvh/features/mapping/bmfile_fix_texture.cpp
|
||||
The implementation of this function is copied from `BallanceVirtoolsHelper/bvh/features/mapping/bmfile_fix_texture.cpp`
|
||||
|
||||
@param mtl[in] The blender material need to be processed.
|
||||
@return True if we do a fix, otherwise return False.
|
||||
:param mtl: The blender material need to be processed.
|
||||
:return: True if we do a fix, otherwise return False.
|
||||
"""
|
||||
# prepare return value first
|
||||
ret: bool = False
|
||||
|
@ -15,7 +15,7 @@ class RawVirtoolsMesh():
|
||||
self.mLitMode = kwargs.get('mLitMode', RawVirtoolsMesh.cDefaultLitMode)
|
||||
|
||||
# blender enum prop helper defines
|
||||
_g_Helper_VXMESH_LITMODE: UTIL_virtools_types.EnumPropHelper = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXMESH_LITMODE)
|
||||
_g_Helper_VXMESH_LITMODE = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXMESH_LITMODE)
|
||||
|
||||
# Blender Property Group
|
||||
|
||||
|
@ -20,8 +20,8 @@ class RawVirtoolsTexture():
|
||||
self.mVideoFormat = kwargs.get('mVideoFormat', RawVirtoolsTexture.cDefaultVideoFormat)
|
||||
|
||||
# blender enum prop helper defines
|
||||
_g_Helper_CK_TEXTURE_SAVEOPTIONS: UTIL_virtools_types.EnumPropHelper = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.CK_TEXTURE_SAVEOPTIONS)
|
||||
_g_Helper_VX_PIXELFORMAT: UTIL_virtools_types.EnumPropHelper = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VX_PIXELFORMAT)
|
||||
_g_Helper_CK_TEXTURE_SAVEOPTIONS = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.CK_TEXTURE_SAVEOPTIONS)
|
||||
_g_Helper_VX_PIXELFORMAT = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VX_PIXELFORMAT)
|
||||
|
||||
class BBP_PG_virtools_texture(bpy.types.PropertyGroup):
|
||||
|
||||
@ -64,12 +64,10 @@ def set_raw_virtools_texture(img: bpy.types.Image, rawdata: RawVirtoolsTexture)
|
||||
|
||||
#region Virtools Texture Drawer
|
||||
|
||||
"""!
|
||||
@remark
|
||||
Because Image do not have its unique properties window
|
||||
so we only can draw virtools texture properties in other window
|
||||
we provide various function to help draw property.
|
||||
"""
|
||||
# YYC MARK:
|
||||
# Because Image do not have its unique properties window,
|
||||
# so we only can draw Virtools Texture properties in other window.
|
||||
# We provide various functions to help draw properties.
|
||||
|
||||
def draw_virtools_texture(img: bpy.types.Image, layout: bpy.types.UILayout):
|
||||
props: BBP_PG_virtools_texture = get_virtools_texture(img)
|
||||
|
@ -187,15 +187,14 @@ class PrototypeShowcaseCfgDescriptor():
|
||||
def get_default(self) -> typing.Any:
|
||||
return _eval_showcase_cfgs_default(self.__mRawCfg[TOKEN_SHOWCASE_CFGS_DEFAULT])
|
||||
|
||||
class EnumPropHelper(UTIL_functions.EnumPropHelper):
|
||||
class EnumPropHelper(UTIL_functions.EnumPropHelper[str]):
|
||||
"""
|
||||
The BME specialized Blender EnumProperty helper.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# init parent class
|
||||
UTIL_functions.EnumPropHelper.__init__(
|
||||
self,
|
||||
super().__init__(
|
||||
self.get_bme_identifiers(),
|
||||
lambda x: x,
|
||||
lambda x: x,
|
||||
|
@ -2,19 +2,17 @@ import bpy, mathutils
|
||||
import math, typing, enum, sys
|
||||
|
||||
class BBPException(Exception):
|
||||
"""
|
||||
The exception thrown by Ballance Blender Plugin
|
||||
"""
|
||||
""" The exception thrown by Ballance Blender Plugin"""
|
||||
pass
|
||||
|
||||
def clamp_float(v: float, min_val: float, max_val: float) -> float:
|
||||
"""!
|
||||
@brief Clamp a float value
|
||||
"""
|
||||
Clamp a float value
|
||||
|
||||
@param v[in] The value need to be clamp.
|
||||
@param min_val[in] The allowed minium value, including self.
|
||||
@param max_val[in] The allowed maxium value, including self.
|
||||
@return Clamped value.
|
||||
:param v: The value need to be clamp.
|
||||
:param min_val: The allowed minium value (inclusive).
|
||||
:param max_val: The allowed maxium value (inclusive).
|
||||
:return: Clamped value.
|
||||
"""
|
||||
if (max_val < min_val): raise BBPException("Invalid range of clamp_float().")
|
||||
|
||||
@ -23,13 +21,13 @@ def clamp_float(v: float, min_val: float, max_val: float) -> float:
|
||||
else: return v
|
||||
|
||||
def clamp_int(v: int, min_val: int, max_val: int) -> int:
|
||||
"""!
|
||||
@brief Clamp a int value
|
||||
"""
|
||||
Clamp a int value
|
||||
|
||||
@param v[in] The value need to be clamp.
|
||||
@param min_val[in] The allowed minium value, including self.
|
||||
@param max_val[in] The allowed maxium value, including self.
|
||||
@return Clamped value.
|
||||
:param v: The value need to be clamp.
|
||||
:param min_val: The allowed minium value (inclusive).
|
||||
:param max_val: The allowed maxium value (inclusive).
|
||||
:return: Clamped value.
|
||||
"""
|
||||
if (max_val < min_val): raise BBPException("Invalid range of clamp_int().")
|
||||
|
||||
@ -41,9 +39,9 @@ def message_box(message: tuple[str, ...], title: str, icon: str):
|
||||
"""
|
||||
Show a message box in Blender. Non-block mode.
|
||||
|
||||
@param message[in] The text this message box displayed. Each item in this param will show as a single line.
|
||||
@param title[in] Message box title text.
|
||||
@param icon[in] The icon this message box displayed.
|
||||
:param message: The text this message box displayed. Each item in this param will show as a single line.
|
||||
:param title: Message box title text.
|
||||
:param icon: The icon this message box displayed.
|
||||
"""
|
||||
def draw(self, context: bpy.types.Context):
|
||||
layout = self.layout
|
||||
@ -53,24 +51,48 @@ def message_box(message: tuple[str, ...], title: str, icon: str):
|
||||
bpy.context.window_manager.popup_menu(draw, title = title, icon = icon)
|
||||
|
||||
def add_into_scene(obj: bpy.types.Object):
|
||||
"""
|
||||
Add given object into active scene.
|
||||
|
||||
:param obj: The 3d object to be added.
|
||||
"""
|
||||
view_layer = bpy.context.view_layer
|
||||
collection = view_layer.active_layer_collection.collection
|
||||
collection.objects.link(obj)
|
||||
|
||||
def move_to_cursor(obj: bpy.types.Object):
|
||||
# use obj.matrix_world to move, not obj.location because this bug:
|
||||
"""
|
||||
Move given object to the position of cursor.
|
||||
|
||||
:param obj: The 3d object to be moved.
|
||||
"""
|
||||
# YYC MARK:
|
||||
# Use `obj.matrix_world` to move, not `obj.location`, because this bug:
|
||||
# https://blender.stackexchange.com/questions/27667/incorrect-matrix-world-after-transformation
|
||||
# the update of matrix_world after setting location is not immediately.
|
||||
# and calling update() function for view_layer for the translation of each object is not suit for too much objects.
|
||||
# The update of `matrix_world` after setting `location` is not immediately.
|
||||
# And it is inviable that calling `update()` function for `view_layer` to update these fields,
|
||||
# because it involve too much objects and cost too much time.
|
||||
|
||||
# obj.location = bpy.context.scene.cursor.location
|
||||
obj.matrix_world = obj.matrix_world @ mathutils.Matrix.Translation(bpy.context.scene.cursor.location - obj.location)
|
||||
|
||||
def add_into_scene_and_move_to_cursor(obj: bpy.types.Object):
|
||||
"""
|
||||
Add given object into active scene and move it to cursor position.
|
||||
|
||||
This function is just a simple combination of previous functions.
|
||||
|
||||
:param obj: The 3d object to be processed.
|
||||
"""
|
||||
add_into_scene(obj)
|
||||
move_to_cursor(obj)
|
||||
|
||||
def select_certain_objects(objs: tuple[bpy.types.Object, ...]) -> None:
|
||||
"""
|
||||
Deselect all objects and then select given 3d objects.
|
||||
|
||||
:param objs: The tuple of 3d objects to be selected.
|
||||
"""
|
||||
# deselect all objects first
|
||||
bpy.ops.object.select_all(action = 'DESELECT')
|
||||
# if no objects, return
|
||||
@ -83,6 +105,11 @@ def select_certain_objects(objs: tuple[bpy.types.Object, ...]) -> None:
|
||||
bpy.context.view_layer.objects.active = objs[0]
|
||||
|
||||
def is_in_object_mode() -> bool:
|
||||
"""
|
||||
Check whether we are in Blender Object Mode.
|
||||
|
||||
:return: True if we are in object mode which suit for exporting something.
|
||||
"""
|
||||
# get active object from context
|
||||
obj = bpy.context.active_object
|
||||
|
||||
@ -92,51 +119,59 @@ def is_in_object_mode() -> bool:
|
||||
# simply check active object mode
|
||||
return obj.mode == 'OBJECT'
|
||||
|
||||
class EnumPropHelper():
|
||||
#region Blender Enum Property Helper
|
||||
|
||||
_TRawEnum = typing.TypeVar('_TRawEnum')
|
||||
|
||||
_TFctToStr = typing.Callable[[_TRawEnum], str]
|
||||
_TFctFromStr = typing.Callable[[str], _TRawEnum]
|
||||
_TFctName = typing.Callable[[_TRawEnum], str]
|
||||
_TFctDesc = typing.Callable[[_TRawEnum], str]
|
||||
_TFctIcon = typing.Callable[[_TRawEnum], str | int]
|
||||
|
||||
class EnumPropHelper(typing.Generic[_TRawEnum]):
|
||||
"""
|
||||
These class contain all functions related to EnumProperty, including generating `items`,
|
||||
parsing data from EnumProperty string value and getting EnumProperty acceptable string format from data.
|
||||
"""
|
||||
|
||||
# define some type hint
|
||||
_TFctToStr = typing.Callable[[typing.Any], str]
|
||||
_TFctFromStr = typing.Callable[[str], typing.Any]
|
||||
_TFctName = typing.Callable[[typing.Any], str]
|
||||
_TFctDesc = typing.Callable[[typing.Any], str]
|
||||
_TFctIcon = typing.Callable[[typing.Any], str | int]
|
||||
# YYC MARK:
|
||||
# I don't know why I can have subscripting for a `typing.Callable` object.
|
||||
# It was not introduced in any document and I just know it from AI.
|
||||
# If I am not doing this, the type hint will crash into Unknown type.
|
||||
# But it works now I don't want to touch it anymore.
|
||||
|
||||
# define class member
|
||||
__mCollections: typing.Iterable[_TRawEnum]
|
||||
__mFctToStr: _TFctToStr[_TRawEnum]
|
||||
__mFctFromStr: _TFctFromStr[_TRawEnum]
|
||||
__mFctName: _TFctName[_TRawEnum]
|
||||
__mFctDesc: _TFctDesc[_TRawEnum]
|
||||
__mFctIcon: _TFctIcon[_TRawEnum]
|
||||
|
||||
__mCollections: typing.Iterable[typing.Any]
|
||||
__mFctToStr: _TFctToStr
|
||||
__mFctFromStr: _TFctFromStr
|
||||
__mFctName: _TFctName
|
||||
__mFctDesc: _TFctDesc
|
||||
__mFctIcon: _TFctIcon
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
collections_: typing.Iterable[typing.Any],
|
||||
fct_to_str: _TFctToStr,
|
||||
fct_from_str: _TFctFromStr,
|
||||
fct_name: _TFctName,
|
||||
fct_desc: _TFctDesc,
|
||||
fct_icon: _TFctIcon):
|
||||
def __init__(self, collections: typing.Iterable[_TRawEnum],
|
||||
fct_to_str: _TFctToStr[_TRawEnum], fct_from_str: _TFctFromStr[_TRawEnum],
|
||||
fct_name: _TFctName[_TRawEnum], fct_desc: _TFctDesc[_TRawEnum],
|
||||
fct_icon: _TFctIcon[_TRawEnum]):
|
||||
"""
|
||||
Initialize a EnumProperty helper.
|
||||
Initialize an EnumProperty helper.
|
||||
|
||||
@param collections_ [in] The collection all available enum property entries contained.
|
||||
It can be enum.Enum or a simple list/tuple/dict.
|
||||
@param fct_to_str [in] A function pointer converting data collection member to its string format.
|
||||
For enum.IntEnum, it can be simply `lambda x: str(x.value)`
|
||||
@param fct_from_str [in] A function pointer getting data collection member from its string format.
|
||||
For enum.IntEnum, it can be simply `lambda x: TEnum(int(x))`
|
||||
@param fct_name [in] A function pointer converting data collection member to its display name.
|
||||
@param fct_desc [in] Same as `fct_name` but return description instead. Return empty string, not None if no description.
|
||||
@param fct_icon [in] Same as `fct_name` but return the used icon instead. Return empty string if no icon.
|
||||
:param collections: The collection containing all available enum property entries.
|
||||
It can be `enum.Enum` or a simple list/tuple.
|
||||
:param fct_to_str: A function pointer converting data collection member to its string format.
|
||||
You must make sure that each members built name is unique in collection!
|
||||
For `enum.IntEnum`, it can be simple `lambda x: str(x.value)`
|
||||
:param fct_from_str: A function pointer getting data collection member from its string format.
|
||||
This class promise that given string must can be parsed.
|
||||
For `enum.IntEnum`, it can be simple `lambda x: TEnum(int(x))`
|
||||
:param fct_name: A function pointer converting data collection member to its display name which shown in Blender.
|
||||
:param fct_desc: Same as `fct_name` but return description instead which shown in Blender
|
||||
If no description, return empty string, not None.
|
||||
:param fct_icon: Same as `fct_name` but return the used icon instead which shown in Blender.
|
||||
It can be a Blender builtin icon string, or any loaded icon integer ID.
|
||||
If no icon, return empty string.
|
||||
"""
|
||||
# assign member
|
||||
self.__mCollections = collections_
|
||||
self.__mCollections = collections
|
||||
self.__mFctToStr = fct_to_str
|
||||
self.__mFctFromStr = fct_from_str
|
||||
self.__mFctName = fct_name
|
||||
@ -159,20 +194,22 @@ class EnumPropHelper():
|
||||
) for idx, member in enumerate(self.__mCollections)
|
||||
)
|
||||
|
||||
def get_selection(self, prop: str) -> typing.Any:
|
||||
def get_selection(self, prop: str) -> _TRawEnum:
|
||||
"""
|
||||
Return collection member from given Blender EnumProp string data.
|
||||
"""
|
||||
# call from_str fct ptr
|
||||
return self.__mFctFromStr(prop)
|
||||
|
||||
def to_selection(self, val: typing.Any) -> str:
|
||||
def to_selection(self, val: _TRawEnum) -> str:
|
||||
"""
|
||||
Parse collection member to Blender EnumProp acceptable string format.
|
||||
"""
|
||||
# call to_str fct ptr
|
||||
return self.__mFctToStr(val)
|
||||
|
||||
#endregion
|
||||
|
||||
#region Blender Collection Visitor
|
||||
|
||||
_TPropertyGroup = typing.TypeVar('_TPropertyGroup', bound = bpy.types.PropertyGroup)
|
||||
@ -190,30 +227,33 @@ class CollectionVisitor(typing.Generic[_TPropertyGroup]):
|
||||
self.__mSrcProp = src_prop
|
||||
|
||||
def add(self) -> _TPropertyGroup:
|
||||
"""!
|
||||
@brief Adds a new item to the collection.
|
||||
@return The instance of newly created item.
|
||||
"""
|
||||
Adds a new item to the collection.
|
||||
|
||||
:return: The instance of newly created item.
|
||||
"""
|
||||
return self.__mSrcProp.add()
|
||||
|
||||
def remove(self, index: int) -> None:
|
||||
"""!
|
||||
@brief Removes the item at the specified index from the collection.
|
||||
@param[in] index The index of the item to remove.
|
||||
"""
|
||||
Removes the item at the specified index from the collection.
|
||||
|
||||
:param index: The index of the item to remove.
|
||||
"""
|
||||
self.__mSrcProp.remove(index)
|
||||
|
||||
def move(self, from_index: int, to_index: int) -> None:
|
||||
"""!
|
||||
@brief Moves an item from one index to another within the collection.
|
||||
@param[in] from_index The current index of the item to move.
|
||||
@param[in] to_index The target index where the item should be moved.
|
||||
"""
|
||||
Moves an item from one index to another within the collection.
|
||||
|
||||
:param from_index: The current index of the item to move.
|
||||
:param to_index: The target index where the item should be moved.
|
||||
"""
|
||||
self.__mSrcProp.move(from_index, to_index)
|
||||
|
||||
def clear(self) -> None:
|
||||
"""!
|
||||
@brief Clears all items from the collection.
|
||||
"""
|
||||
Clears all items from the collection.
|
||||
"""
|
||||
self.__mSrcProp.clear()
|
||||
|
||||
@ -238,8 +278,8 @@ _TMutexObject = typing.TypeVar('_TMutexObject')
|
||||
|
||||
class TinyMutex(typing.Generic[_TMutexObject]):
|
||||
"""
|
||||
In this plugin, some class have "with" context feature.
|
||||
However, it is essential to block any futher visiting if some "with" context are operating on some object.
|
||||
In this plugin, some classes have "with" context feature.
|
||||
However, in some cases, it is essential to block any futher visiting if some "with" context are operating on some object.
|
||||
This is the reason why this tiny mutex is designed.
|
||||
|
||||
Please note this class is not a real MUTEX.
|
||||
@ -253,17 +293,35 @@ class TinyMutex(typing.Generic[_TMutexObject]):
|
||||
self.__mProtectedObjects = set()
|
||||
|
||||
def lock(self, obj: _TMutexObject) -> None:
|
||||
"""
|
||||
Lock given object.
|
||||
|
||||
:raise BBPException: Raised if given object has been locked.
|
||||
:param obj: The resource to be locked.
|
||||
"""
|
||||
if obj in self.__mProtectedObjects:
|
||||
raise BBPException('It is not allowed that operate multiple "with" contexts on a single object.')
|
||||
self.__mProtectedObjects.add(obj)
|
||||
|
||||
def try_lock(self, obj: _TMutexObject) -> bool:
|
||||
"""
|
||||
Try lock given object.
|
||||
|
||||
:param obj: The resource to be locked.
|
||||
:return: True if we successfully lock it, otherwise false.
|
||||
"""
|
||||
if obj in self.__mProtectedObjects:
|
||||
return False
|
||||
self.__mProtectedObjects.add(obj)
|
||||
return True
|
||||
|
||||
def unlock(self, obj: _TMutexObject) -> None:
|
||||
"""
|
||||
Unlock given object.
|
||||
|
||||
:raise BBPException: Raised if given object is not locked.
|
||||
:param obj: The resource to be unlocked.
|
||||
"""
|
||||
if obj not in self.__mProtectedObjects:
|
||||
raise BBPException('It is not allowed that unlock an non-existent object.')
|
||||
self.__mProtectedObjects.remove(obj)
|
||||
|
@ -3,11 +3,13 @@ import enum, typing
|
||||
from . import UTIL_virtools_types, UTIL_functions
|
||||
from . import PROP_ptrprop_resolver, PROP_ballance_map_info
|
||||
|
||||
## Intent
|
||||
# Some importer or exporter may share same properties.
|
||||
# So we create some shared class and user just need inherit them
|
||||
# and call general getter to get user selected data.
|
||||
# Also provide draw function thus caller do not need draw the params themselves.
|
||||
# INTENT:
|
||||
# Some importer or exporter may share same properties.
|
||||
# So we create some shared class and user just need inherit them
|
||||
# and call general getter to get user selected data.
|
||||
# Also provide draw function thus caller do not need draw the params themselves.
|
||||
|
||||
#region Import Params
|
||||
|
||||
class ConflictStrategy(enum.IntEnum):
|
||||
Rename = enum.auto()
|
||||
@ -16,7 +18,7 @@ _g_ConflictStrategyDesc: dict[ConflictStrategy, tuple[str, str]] = {
|
||||
ConflictStrategy.Rename: ('Rename', 'Rename the new one'),
|
||||
ConflictStrategy.Current: ('Use Current', 'Use current one'),
|
||||
}
|
||||
_g_EnumHelper_ConflictStrategy: UTIL_functions.EnumPropHelper = UTIL_functions.EnumPropHelper(
|
||||
_g_EnumHelper_ConflictStrategy = UTIL_functions.EnumPropHelper(
|
||||
ConflictStrategy,
|
||||
lambda x: str(x.value),
|
||||
lambda x: ConflictStrategy(int(x)),
|
||||
@ -25,39 +27,6 @@ _g_EnumHelper_ConflictStrategy: UTIL_functions.EnumPropHelper = UTIL_functions.E
|
||||
lambda _: ''
|
||||
)
|
||||
|
||||
#region Assist Classes
|
||||
|
||||
class ExportEditModeBackup():
|
||||
"""
|
||||
The class which save Edit Mode when exporting and restore it after exporting.
|
||||
Because edit mode is not allowed when exporting.
|
||||
Support `with` statement.
|
||||
|
||||
```
|
||||
with ExportEditModeBackup():
|
||||
# do some exporting work
|
||||
blabla()
|
||||
# restore automatically when exiting "with"
|
||||
```
|
||||
"""
|
||||
mInEditMode: bool
|
||||
|
||||
def __init__(self):
|
||||
if bpy.context.object and bpy.context.object.mode == "EDIT":
|
||||
# set and toggle it. otherwise exporting will failed.
|
||||
self.mInEditMode = True
|
||||
bpy.ops.object.editmode_toggle()
|
||||
else:
|
||||
self.mInEditMode = False
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
if self.mInEditMode:
|
||||
bpy.ops.object.editmode_toggle()
|
||||
self.mInEditMode = False
|
||||
|
||||
class ConflictResolver():
|
||||
"""
|
||||
This class frequently used when importing objects.
|
||||
@ -151,8 +120,6 @@ class ConflictResolver():
|
||||
tex.name = name
|
||||
return (tex, True)
|
||||
|
||||
#endregion
|
||||
|
||||
class ImportParams():
|
||||
texture_conflict_strategy: bpy.props.EnumProperty(
|
||||
name = "Texture Name Conflict",
|
||||
@ -239,13 +206,65 @@ class ImportParams():
|
||||
self.general_get_texture_conflict_strategy()
|
||||
)
|
||||
|
||||
#endregion
|
||||
|
||||
#region Export Params
|
||||
|
||||
class ExportEditModeBackup():
|
||||
"""
|
||||
The class which save Edit Mode when exporting and restore it after exporting.
|
||||
Because edit mode is not allowed when exporting.
|
||||
Support `with` statement.
|
||||
|
||||
```
|
||||
with ExportEditModeBackup():
|
||||
# do some exporting work
|
||||
blabla()
|
||||
# restore automatically when exiting "with"
|
||||
```
|
||||
"""
|
||||
mInEditMode: bool
|
||||
|
||||
def __init__(self):
|
||||
if bpy.context.object and bpy.context.object.mode == "EDIT":
|
||||
# set and toggle it. otherwise exporting will failed.
|
||||
self.mInEditMode = True
|
||||
bpy.ops.object.editmode_toggle()
|
||||
else:
|
||||
self.mInEditMode = False
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
if self.mInEditMode:
|
||||
bpy.ops.object.editmode_toggle()
|
||||
self.mInEditMode = False
|
||||
|
||||
class ExportMode(enum.IntEnum):
|
||||
BldColl = enum.auto()
|
||||
BldObj = enum.auto()
|
||||
BldSelObjs = enum.auto()
|
||||
_g_ExportModeDesc: dict[ExportMode, tuple[str, str, str]] = {
|
||||
ExportMode.BldColl: ('Collection', 'Export a collection', 'OUTLINER_COLLECTION'),
|
||||
ExportMode.BldObj: ('Object', 'Export an object', 'OBJECT_DATA'),
|
||||
ExportMode.BldSelObjs: ('Selected Objects', 'Export selected objects', 'SELECT_SET'),
|
||||
}
|
||||
_g_EnumHelper_ExportMode = UTIL_functions.EnumPropHelper(
|
||||
ExportMode,
|
||||
lambda x: str(x.value),
|
||||
lambda x: ExportMode(int(x)),
|
||||
lambda x: _g_ExportModeDesc[x][0],
|
||||
lambda x: _g_ExportModeDesc[x][1],
|
||||
lambda x: _g_ExportModeDesc[x][2]
|
||||
)
|
||||
|
||||
class ExportParams():
|
||||
export_mode: bpy.props.EnumProperty(
|
||||
name = "Export Mode",
|
||||
items = (
|
||||
('COLLECTION', "Collection", "Export a collection", 'OUTLINER_COLLECTION', 0),
|
||||
('OBJECT', "Object", "Export an object", 'OBJECT_DATA', 1),
|
||||
),
|
||||
description = "Define which 3D objects should be exported",
|
||||
items = _g_EnumHelper_ExportMode.generate_items(),
|
||||
default = _g_EnumHelper_ExportMode.to_selection(ExportMode.BldColl),
|
||||
translation_context = 'BBP/UTIL_ioport_shared.ExportParams/property'
|
||||
) # type: ignore
|
||||
|
||||
@ -262,29 +281,40 @@ class ExportParams():
|
||||
horizon_body.prop(self, "export_mode", expand=True)
|
||||
|
||||
# draw picker
|
||||
export_mode = _g_EnumHelper_ExportMode.get_selection(self.export_mode)
|
||||
ptrprops = PROP_ptrprop_resolver.PropsVisitor(context.scene)
|
||||
if self.export_mode == 'COLLECTION':
|
||||
ptrprops.draw_export_collection(body)
|
||||
elif self.export_mode == 'OBJECT':
|
||||
ptrprops.draw_export_object(body)
|
||||
match export_mode:
|
||||
case ExportMode.BldColl:
|
||||
ptrprops.draw_export_collection(body)
|
||||
case ExportMode.BldObj:
|
||||
ptrprops.draw_export_object(body)
|
||||
case ExportMode.BldSelObjs:
|
||||
pass # Draw nothing
|
||||
|
||||
def general_get_export_objects(self, context: bpy.types.Context) -> tuple[bpy.types.Object] | None:
|
||||
def general_get_export_objects(self, context: bpy.types.Context) -> tuple[bpy.types.Object, ...] | None:
|
||||
"""
|
||||
Return resolved exported objects or None if no selection.
|
||||
"""
|
||||
export_mode = _g_EnumHelper_ExportMode.get_selection(self.export_mode)
|
||||
ptrprops = PROP_ptrprop_resolver.PropsVisitor(context.scene)
|
||||
if self.export_mode == 'COLLECTION':
|
||||
col: bpy.types.Collection = ptrprops.get_export_collection()
|
||||
if col is None: return None
|
||||
else:
|
||||
return tuple(col.all_objects)
|
||||
else:
|
||||
obj: bpy.types.Object = ptrprops.get_export_object()
|
||||
if obj is None: return None
|
||||
else: return (obj, )
|
||||
match export_mode:
|
||||
case ExportMode.BldColl:
|
||||
col: bpy.types.Collection = ptrprops.get_export_collection()
|
||||
if col is None: return None
|
||||
else: return tuple(col.all_objects)
|
||||
case ExportMode.BldObj:
|
||||
obj: bpy.types.Object = ptrprops.get_export_object()
|
||||
if obj is None: return None
|
||||
else: return (obj, )
|
||||
case ExportMode.BldSelObjs:
|
||||
return tuple(context.selected_objects)
|
||||
|
||||
#endregion
|
||||
|
||||
#region Virtools Params
|
||||
|
||||
# define global tex save opt blender enum prop helper
|
||||
_g_EnumHelper_CK_TEXTURE_SAVEOPTIONS: UTIL_virtools_types.EnumPropHelper = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.CK_TEXTURE_SAVEOPTIONS)
|
||||
_g_EnumHelper_CK_TEXTURE_SAVEOPTIONS = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.CK_TEXTURE_SAVEOPTIONS)
|
||||
|
||||
class VirtoolsParams():
|
||||
texture_save_opt: bpy.props.EnumProperty(
|
||||
@ -349,6 +379,10 @@ class VirtoolsParams():
|
||||
def general_get_compress_level(self) -> int:
|
||||
return self.compress_level
|
||||
|
||||
#endregion
|
||||
|
||||
#region Ballance Params
|
||||
|
||||
class BallanceParams():
|
||||
successive_sector: bpy.props.BoolProperty(
|
||||
name="Successive Sector",
|
||||
@ -387,3 +421,5 @@ class BallanceParams():
|
||||
map_info: PROP_ballance_map_info.RawBallanceMapInfo
|
||||
map_info = PROP_ballance_map_info.get_raw_ballance_map_info(bpy.context.scene)
|
||||
return map_info.mSectorCount
|
||||
|
||||
#endregion
|
||||
|
@ -235,21 +235,28 @@ _g_Annotation: dict[type, dict[int, EnumAnnotation]] = {
|
||||
}
|
||||
}
|
||||
|
||||
class EnumPropHelper(UTIL_functions.EnumPropHelper):
|
||||
_TRawEnum = typing.TypeVar('_TRawEnum', bound = enum.Enum)
|
||||
|
||||
class EnumPropHelper(UTIL_functions.EnumPropHelper[_TRawEnum]):
|
||||
"""
|
||||
Virtools type specified Blender EnumProp helper.
|
||||
"""
|
||||
__mAnnotationDict: dict[int, EnumAnnotation]
|
||||
__mEnumTy: type[enum.Enum]
|
||||
__mEnumTy: type[_TRawEnum]
|
||||
|
||||
def __init__(self, ty: type[enum.Enum]):
|
||||
def __init__(self, ty: type[_TRawEnum]):
|
||||
# set enum type and annotation ref first
|
||||
self.__mEnumTy = ty
|
||||
self.__mAnnotationDict = _g_Annotation[ty]
|
||||
# init parent data
|
||||
UTIL_functions.EnumPropHelper.__init__(
|
||||
self,
|
||||
self.__mEnumTy, # enum.Enum it self is iterable
|
||||
|
||||
# YYC MARK:
|
||||
# It seems that Pylance has bad generic analyse ability in there.
|
||||
# It can not deduce the correct generic type in lambda.
|
||||
# I gave up.
|
||||
|
||||
# Init parent data
|
||||
super().__init__(
|
||||
self.__mEnumTy, # enum.Enum its self is iterable
|
||||
lambda x: str(x.value), # convert enum.Enum's value to string
|
||||
lambda x: self.__mEnumTy(int(x)), # use stored enum type and int() to get enum member
|
||||
lambda x: self.__mAnnotationDict[x.value].mDisplayName,
|
||||
@ -265,11 +272,11 @@ def virtools_name_regulator(name: str | None) -> str:
|
||||
if name: return name
|
||||
else: return bpy.app.translations.pgettext_data('annoymous', 'BME/UTIL_virtools_types.virtools_name_regulator()')
|
||||
|
||||
## Default Encoding for PyBMap
|
||||
# Use semicolon split each encodings. Support Western European and Simplified Chinese in default.
|
||||
# Since LibCmo 0.2, the encoding name of LibCmo become universal encoding which is platfoorm independent.
|
||||
# So no need set it according to different platform.
|
||||
# Use universal encoding name (like Python).
|
||||
# YYC MARK:
|
||||
# There are default encodings for PyBMap. We support Western European and Simplified Chinese in default.
|
||||
# Since LibCmo 0.2, the encoding name of LibCmo become universal encoding which is platfoorm independent.
|
||||
# So no need set it according to different platform.
|
||||
# Use universal encoding name (like Python).
|
||||
g_PyBMapDefaultEncodings: tuple[str, ...] = (
|
||||
'cp1252',
|
||||
'gbk'
|
||||
|
@ -1,8 +1,8 @@
|
||||
#region Reload and Import
|
||||
#region Import and Reload
|
||||
|
||||
# import core lib
|
||||
import bpy
|
||||
import typing, collections
|
||||
import typing, enum
|
||||
|
||||
# reload if needed
|
||||
# TODO: finish reload feature if needed.
|
||||
@ -10,8 +10,6 @@ import typing, collections
|
||||
if "bpy" in locals():
|
||||
import importlib
|
||||
|
||||
#endregion
|
||||
|
||||
# we must load icons manager first
|
||||
# and register it
|
||||
from . import UTIL_icons_manager
|
||||
@ -27,9 +25,143 @@ from . import OP_MTL_fix_materials
|
||||
from . import OP_ADDS_component, OP_ADDS_bme, OP_ADDS_rail
|
||||
from . import OP_OBJECT_legacy_align, OP_OBJECT_virtools_group, OP_OBJECT_snoop_group_then_to_mesh, OP_OBJECT_naming_convention
|
||||
|
||||
#region Menu
|
||||
#endregion
|
||||
|
||||
# ===== Menu Defines =====
|
||||
#region Menu and Sidebar Panel
|
||||
|
||||
#region Ballance Adder Menu and Panel
|
||||
|
||||
class DrawTarget(enum.IntEnum):
|
||||
BldMenu = enum.auto()
|
||||
BldPanel = enum.auto()
|
||||
|
||||
def reuse_create_layout(layout: bpy.types.UILayout, target: DrawTarget) -> bpy.types.UILayout:
|
||||
# If we are draw for Panel, we need use Grid to use space enough.
|
||||
match target:
|
||||
case DrawTarget.BldMenu:
|
||||
return layout
|
||||
case DrawTarget.BldPanel:
|
||||
return layout.grid_flow(even_columns=True, even_rows=True)
|
||||
|
||||
def reuse_draw_add_bme(layout: bpy.types.UILayout, target: DrawTarget):
|
||||
# Draw operators.
|
||||
OP_ADDS_bme.BBP_OT_add_bme_struct.draw_blc_menu(reuse_create_layout(layout, target))
|
||||
|
||||
def reuse_draw_add_rail(layout: bpy.types.UILayout, target: DrawTarget):
|
||||
layout.label(text="Sections", icon='MESH_CIRCLE', text_ctxt='BBP/__init__.reuse_draw_add_rail()')
|
||||
sublayout = reuse_create_layout(layout, target)
|
||||
sublayout.operator(OP_ADDS_rail.BBP_OT_add_rail_section.bl_idname)
|
||||
sublayout.operator(OP_ADDS_rail.BBP_OT_add_transition_section.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.label(text="Straight Rails", icon='IPO_CONSTANT', text_ctxt='BBP/__init__.reuse_draw_add_rail()')
|
||||
sublayout = reuse_create_layout(layout, target)
|
||||
sublayout.operator(OP_ADDS_rail.BBP_OT_add_straight_rail.bl_idname)
|
||||
sublayout.operator(OP_ADDS_rail.BBP_OT_add_transition_rail.bl_idname)
|
||||
sublayout.operator(OP_ADDS_rail.BBP_OT_add_side_rail.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.label(text="Curve Rails", icon='MOD_SCREW', text_ctxt='BBP/__init__.reuse_draw_add_rail()')
|
||||
sublayout = reuse_create_layout(layout, target)
|
||||
sublayout.operator(OP_ADDS_rail.BBP_OT_add_arc_rail.bl_idname)
|
||||
sublayout.operator(OP_ADDS_rail.BBP_OT_add_spiral_rail.bl_idname)
|
||||
sublayout.operator(OP_ADDS_rail.BBP_OT_add_side_spiral_rail.bl_idname)
|
||||
|
||||
def reuse_draw_add_component(layout: bpy.types.UILayout, target: DrawTarget):
|
||||
# We only use Grid for basic components
|
||||
layout.label(text="Basic Components", text_ctxt='BBP/__init__.reuse_draw_add_component()')
|
||||
OP_ADDS_component.BBP_OT_add_component.draw_blc_menu(reuse_create_layout(layout, target))
|
||||
|
||||
layout.separator()
|
||||
layout.label(text="Nong Components", text_ctxt='BBP/__init__.reuse_draw_add_component()')
|
||||
sublayout = reuse_create_layout(layout, target)
|
||||
OP_ADDS_component.BBP_OT_add_nong_extra_point.draw_blc_menu(sublayout)
|
||||
OP_ADDS_component.BBP_OT_add_nong_ventilator.draw_blc_menu(sublayout)
|
||||
|
||||
layout.separator()
|
||||
layout.label(text="Series Components", text_ctxt='BBP/__init__.reuse_draw_add_component()')
|
||||
sublayout = reuse_create_layout(layout, target)
|
||||
OP_ADDS_component.BBP_OT_add_tilting_block_series.draw_blc_menu(sublayout)
|
||||
OP_ADDS_component.BBP_OT_add_swing_series.draw_blc_menu(sublayout)
|
||||
OP_ADDS_component.BBP_OT_add_ventilator_series.draw_blc_menu(sublayout)
|
||||
|
||||
layout.separator()
|
||||
layout.label(text="Components Pair", text_ctxt='BBP/__init__.reuse_draw_add_component()')
|
||||
sublayout = reuse_create_layout(layout, target)
|
||||
OP_ADDS_component.BBP_OT_add_sector_component_pair.draw_blc_menu(sublayout)
|
||||
|
||||
class BBP_MT_AddBmeMenu(bpy.types.Menu):
|
||||
"""Add Ballance Floor"""
|
||||
bl_idname = "BBP_MT_AddBmeMenu"
|
||||
bl_label = "Floors"
|
||||
bl_translation_context = 'BBP_MT_AddBmeMenu'
|
||||
|
||||
def draw(self, context):
|
||||
reuse_draw_add_bme(self.layout, DrawTarget.BldMenu)
|
||||
|
||||
class BBP_MT_AddRailMenu(bpy.types.Menu):
|
||||
"""Add Ballance Rail"""
|
||||
bl_idname = "BBP_MT_AddRailMenu"
|
||||
bl_label = "Rails"
|
||||
bl_translation_context = 'BBP_MT_AddRailMenu'
|
||||
|
||||
def draw(self, context):
|
||||
reuse_draw_add_rail(self.layout, DrawTarget.BldMenu)
|
||||
|
||||
class BBP_MT_AddComponentMenu(bpy.types.Menu):
|
||||
"""Add Ballance Component"""
|
||||
bl_idname = "BBP_MT_AddComponentsMenu"
|
||||
bl_label = "Components"
|
||||
bl_translation_context = 'BBP_MT_AddComponentsMenu'
|
||||
|
||||
def draw(self, context):
|
||||
reuse_draw_add_component(self.layout, DrawTarget.BldMenu)
|
||||
|
||||
class BBP_PT_SidebarAddBmePanel(bpy.types.Panel):
|
||||
"""Add Ballance Floor"""
|
||||
bl_label = "Floors"
|
||||
bl_idname = "BBP_PT_SidebarAddBmePanel"
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_region_type = 'UI'
|
||||
bl_context = "objectmode"
|
||||
bl_category = 'Ballance'
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
bl_translation_context = 'BBP_PT_SidebarAddBmePanel'
|
||||
|
||||
def draw(self, context):
|
||||
reuse_draw_add_bme(self.layout, DrawTarget.BldPanel)
|
||||
|
||||
class BBP_PT_SidebarAddRailPanel(bpy.types.Panel):
|
||||
"""Add Ballance Rail"""
|
||||
bl_label = "Rails"
|
||||
bl_idname = "BBP_PT_SidebarAddRailPanel"
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_region_type = 'UI'
|
||||
bl_context = "objectmode"
|
||||
bl_category = 'Ballance'
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
bl_translation_context = 'BBP_PT_SidebarAddRailPanel'
|
||||
|
||||
def draw(self, context):
|
||||
reuse_draw_add_rail(self.layout, DrawTarget.BldPanel)
|
||||
|
||||
class BBP_PT_SidebarAddComponentPanel(bpy.types.Panel):
|
||||
"""Add Ballance Component"""
|
||||
bl_label = "Components"
|
||||
bl_idname = "BBP_PT_SidebarAddComponentPanel"
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_region_type = 'UI'
|
||||
bl_context = "objectmode"
|
||||
bl_category = 'Ballance'
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
bl_translation_context = 'BBP_PT_SidebarAddComponentPanel'
|
||||
|
||||
def draw(self, context):
|
||||
reuse_draw_add_component(self.layout, DrawTarget.BldPanel)
|
||||
|
||||
#endregion
|
||||
|
||||
#region Other Menu
|
||||
|
||||
class BBP_MT_View3DMenu(bpy.types.Menu):
|
||||
"""Ballance 3D related operators"""
|
||||
@ -52,71 +184,11 @@ class BBP_MT_View3DMenu(bpy.types.Menu):
|
||||
layout.label(text='Material', icon='MATERIAL', text_ctxt='BBP_MT_View3DMenu/draw')
|
||||
layout.operator(OP_MTL_fix_materials.BBP_OT_fix_all_materials.bl_idname)
|
||||
|
||||
class BBP_MT_AddBmeMenu(bpy.types.Menu):
|
||||
"""Add Ballance Floor"""
|
||||
bl_idname = "BBP_MT_AddBmeMenu"
|
||||
bl_label = "Floors"
|
||||
bl_translation_context = 'BBP_MT_AddBmeMenu'
|
||||
#endregion
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
OP_ADDS_bme.BBP_OT_add_bme_struct.draw_blc_menu(layout)
|
||||
#region Menu Drawer
|
||||
|
||||
class BBP_MT_AddRailMenu(bpy.types.Menu):
|
||||
"""Add Ballance Rail"""
|
||||
bl_idname = "BBP_MT_AddRailMenu"
|
||||
bl_label = "Rails"
|
||||
bl_translation_context = 'BBP_MT_AddRailMenu'
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
layout.label(text="Sections", icon='MESH_CIRCLE', text_ctxt='BBP_MT_AddRailMenu/draw')
|
||||
layout.operator(OP_ADDS_rail.BBP_OT_add_rail_section.bl_idname)
|
||||
layout.operator(OP_ADDS_rail.BBP_OT_add_transition_section.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.label(text="Straight Rails", icon='IPO_CONSTANT', text_ctxt='BBP_MT_AddRailMenu/draw')
|
||||
layout.operator(OP_ADDS_rail.BBP_OT_add_straight_rail.bl_idname)
|
||||
layout.operator(OP_ADDS_rail.BBP_OT_add_transition_rail.bl_idname)
|
||||
layout.operator(OP_ADDS_rail.BBP_OT_add_side_rail.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.label(text="Curve Rails", icon='MOD_SCREW', text_ctxt='BBP_MT_AddRailMenu/draw')
|
||||
layout.operator(OP_ADDS_rail.BBP_OT_add_arc_rail.bl_idname)
|
||||
layout.operator(OP_ADDS_rail.BBP_OT_add_spiral_rail.bl_idname)
|
||||
layout.operator(OP_ADDS_rail.BBP_OT_add_side_spiral_rail.bl_idname)
|
||||
|
||||
class BBP_MT_AddComponentsMenu(bpy.types.Menu):
|
||||
"""Add Ballance Component"""
|
||||
bl_idname = "BBP_MT_AddComponentsMenu"
|
||||
bl_label = "Components"
|
||||
bl_translation_context = 'BBP_MT_AddComponentsMenu'
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
layout.label(text="Basic Components", text_ctxt='BBP_MT_AddComponentsMenu/draw')
|
||||
OP_ADDS_component.BBP_OT_add_component.draw_blc_menu(layout)
|
||||
|
||||
layout.separator()
|
||||
layout.label(text="Nong Components", text_ctxt='BBP_MT_AddComponentsMenu/draw')
|
||||
OP_ADDS_component.BBP_OT_add_nong_extra_point.draw_blc_menu(layout)
|
||||
OP_ADDS_component.BBP_OT_add_nong_ventilator.draw_blc_menu(layout)
|
||||
|
||||
layout.separator()
|
||||
layout.label(text="Series Components", text_ctxt='BBP_MT_AddComponentsMenu/draw')
|
||||
OP_ADDS_component.BBP_OT_add_tilting_block_series.draw_blc_menu(layout)
|
||||
OP_ADDS_component.BBP_OT_add_swing_series.draw_blc_menu(layout)
|
||||
OP_ADDS_component.BBP_OT_add_ventilator_series.draw_blc_menu(layout)
|
||||
|
||||
layout.separator()
|
||||
layout.label(text="Components Pair", text_ctxt='BBP_MT_AddComponentsMenu/draw')
|
||||
OP_ADDS_component.BBP_OT_add_sector_component_pair.draw_blc_menu(layout)
|
||||
|
||||
# ===== Menu Drawer =====
|
||||
|
||||
MenuDrawer_t = typing.Callable[[typing.Any, typing.Any], None]
|
||||
TFctMenuDrawer = typing.Callable[[typing.Any, typing.Any], None]
|
||||
|
||||
def menu_drawer_import(self, context) -> None:
|
||||
layout: bpy.types.UILayout = self.layout
|
||||
@ -154,16 +226,17 @@ def menu_drawer_add(self, context) -> None:
|
||||
layout.label(text="Ballance", text_ctxt='BBP/__init__.menu_drawer_add()')
|
||||
layout.menu(BBP_MT_AddBmeMenu.bl_idname, icon='MESH_CUBE')
|
||||
layout.menu(BBP_MT_AddRailMenu.bl_idname, icon='MESH_CIRCLE')
|
||||
layout.menu(BBP_MT_AddComponentsMenu.bl_idname, icon='MESH_ICOSPHERE')
|
||||
layout.menu(BBP_MT_AddComponentMenu.bl_idname, icon='MESH_ICOSPHERE')
|
||||
|
||||
def menu_drawer_grouping(self, context) -> None:
|
||||
layout: bpy.types.UILayout = self.layout
|
||||
layout.separator()
|
||||
|
||||
# NOTE: because outline context may change operator context
|
||||
# YYC MARK:
|
||||
# Because outline context change operator context into EXEC_*,
|
||||
# so it will cause no popup window when click operator in outline.
|
||||
# thus we create a sub layout and set its operator context as 'INVOKE_DEFAULT'
|
||||
# thus, all operators can pop up normally.
|
||||
# Thus we create a sub layout and set its operator context as 'INVOKE_DEFAULT',
|
||||
# so that all operators can pop up normally.
|
||||
col = layout.column()
|
||||
col.operator_context = 'INVOKE_DEFAULT'
|
||||
|
||||
@ -188,7 +261,8 @@ def menu_drawer_naming_convention(self, context) -> None:
|
||||
layout: bpy.types.UILayout = self.layout
|
||||
layout.separator()
|
||||
|
||||
# same reason in `menu_drawer_grouping()``
|
||||
# YYC MARK:
|
||||
# Same reason for changing operator context introduced in `menu_drawer_grouping()`
|
||||
col = layout.column()
|
||||
col.operator_context = 'INVOKE_DEFAULT'
|
||||
|
||||
@ -199,37 +273,43 @@ def menu_drawer_naming_convention(self, context) -> None:
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
#region Register and Unregister.
|
||||
|
||||
g_BldClasses: tuple[typing.Any, ...] = (
|
||||
BBP_MT_View3DMenu,
|
||||
BBP_MT_AddBmeMenu,
|
||||
BBP_MT_AddRailMenu,
|
||||
BBP_MT_AddComponentsMenu
|
||||
BBP_MT_AddComponentMenu,
|
||||
|
||||
BBP_PT_SidebarAddBmePanel,
|
||||
BBP_PT_SidebarAddRailPanel,
|
||||
BBP_PT_SidebarAddComponentPanel,
|
||||
)
|
||||
|
||||
class MenuEntry():
|
||||
mContainerMenu: bpy.types.Menu
|
||||
mIsAppend: bool
|
||||
mMenuDrawer: MenuDrawer_t
|
||||
def __init__(self, cont: bpy.types.Menu, is_append: bool, menu_func: MenuDrawer_t):
|
||||
mMenuDrawer: TFctMenuDrawer
|
||||
def __init__(self, cont: bpy.types.Menu, is_append: bool, menu_func: TFctMenuDrawer):
|
||||
self.mContainerMenu = cont
|
||||
self.mIsAppend = is_append
|
||||
self.mMenuDrawer = menu_func
|
||||
|
||||
g_BldMenus: tuple[MenuEntry, ...] = (
|
||||
MenuEntry(bpy.types.VIEW3D_MT_editor_menus, False, menu_drawer_view3d),
|
||||
MenuEntry(bpy.types.TOPBAR_MT_file_import, True, menu_drawer_import),
|
||||
MenuEntry(bpy.types.TOPBAR_MT_file_export, True, menu_drawer_export),
|
||||
MenuEntry(bpy.types.VIEW3D_MT_add, True, menu_drawer_add),
|
||||
MenuEntry(bpy.types.VIEW3D_MT_editor_menus, False, menu_drawer_view3d),
|
||||
MenuEntry(bpy.types.TOPBAR_MT_file_import, True, menu_drawer_import),
|
||||
MenuEntry(bpy.types.TOPBAR_MT_file_export, True, menu_drawer_export),
|
||||
MenuEntry(bpy.types.VIEW3D_MT_add, True, menu_drawer_add),
|
||||
|
||||
MenuEntry(bpy.types.VIEW3D_MT_object_context_menu, True, menu_drawer_snoop_then_conv),
|
||||
MenuEntry(bpy.types.VIEW3D_MT_object_context_menu, True, menu_drawer_snoop_then_conv),
|
||||
|
||||
# register double (for 2 menus)
|
||||
MenuEntry(bpy.types.VIEW3D_MT_object_context_menu, True, menu_drawer_grouping),
|
||||
MenuEntry(bpy.types.OUTLINER_MT_object, True, menu_drawer_grouping),
|
||||
# Register this twice (for 2 menus respectively)
|
||||
MenuEntry(bpy.types.VIEW3D_MT_object_context_menu, True, menu_drawer_grouping),
|
||||
MenuEntry(bpy.types.OUTLINER_MT_object, True, menu_drawer_grouping),
|
||||
|
||||
MenuEntry(bpy.types.OUTLINER_MT_collection, True, menu_drawer_naming_convention),
|
||||
MenuEntry(bpy.types.OUTLINER_MT_collection, True, menu_drawer_naming_convention),
|
||||
)
|
||||
|
||||
def register() -> None:
|
||||
|
@ -2,12 +2,13 @@ import json, logging
|
||||
from pathlib import Path
|
||||
import common
|
||||
from common import AssetKind
|
||||
import json5
|
||||
|
||||
|
||||
def _compress_json(src_file: Path, dst_file: Path) -> None:
|
||||
# load data first
|
||||
with open(src_file, 'r', encoding='utf-8') as f:
|
||||
loaded_prototypes = json.load(f)
|
||||
loaded_prototypes = json5.load(f)
|
||||
|
||||
# save result with compress config
|
||||
with open(dst_file, 'w', encoding='utf-8') as f:
|
||||
@ -24,13 +25,14 @@ def build_jsons() -> None:
|
||||
raw_jsons_dir = common.get_raw_assets_folder(AssetKind.Jsons)
|
||||
plg_jsons_dir = common.get_plugin_assets_folder(AssetKind.Jsons)
|
||||
|
||||
for raw_json_file in raw_jsons_dir.glob('*.json'):
|
||||
for raw_json_file in raw_jsons_dir.glob('*.json5'):
|
||||
# Skip non-file.
|
||||
if not raw_json_file.is_file():
|
||||
continue
|
||||
|
||||
# Build final path
|
||||
plg_json_file = plg_jsons_dir / raw_json_file.relative_to(raw_jsons_dir)
|
||||
plg_json_file = plg_json_file.with_suffix('.json')
|
||||
|
||||
# Show message
|
||||
logging.info(f'Compressing {raw_json_file} -> {plg_json_file}')
|
||||
|
@ -1,4 +1,4 @@
|
||||
import os, typing, logging, enum
|
||||
import logging, enum, typing
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import json, logging, typing, itertools
|
||||
import logging, typing, itertools
|
||||
from pathlib import Path
|
||||
import common, bme
|
||||
from common import AssetKind
|
||||
import pydantic, polib
|
||||
import pydantic, polib, json5
|
||||
|
||||
## YYC MARK:
|
||||
# This translation context string prefix is cpoied from UTIL_translation.py.
|
||||
@ -38,14 +38,14 @@ def _extract_json(json_file: Path) -> typing.Iterator[polib.POEntry]:
|
||||
try:
|
||||
# Read file and convert it into BME struct.
|
||||
with open(json_file, 'r', encoding='utf-8') as f:
|
||||
document = json.load(f)
|
||||
document = json5.load(f)
|
||||
prototypes = bme.Prototypes.model_validate(document)
|
||||
# Extract translation
|
||||
return itertools.chain.from_iterable(_extract_prototype(prototype) for prototype in prototypes.root)
|
||||
except json.JSONDecodeError:
|
||||
logging.error(f'Can not extract translation from {json_file} due to JSON error. Please validate it first.')
|
||||
except pydantic.ValidationError:
|
||||
logging.error(f'Can not extract translation from {json_file} due to struct error. Please validate it first.')
|
||||
except (ValueError, UnicodeDecodeError):
|
||||
logging.error(f'Can not extract translation from {json_file} due to JSON5 error. Please validate it first.')
|
||||
|
||||
# Output nothing
|
||||
return itertools.chain.from_iterable(())
|
||||
@ -70,7 +70,7 @@ def extract_jsons() -> None:
|
||||
}
|
||||
|
||||
# Iterate all prototypes and add into POT
|
||||
for raw_json_file in raw_jsons_dir.glob('*.json'):
|
||||
for raw_json_file in raw_jsons_dir.glob('*.json5'):
|
||||
# Skip non-file.
|
||||
if not raw_json_file.is_file():
|
||||
continue
|
||||
|
@ -3,6 +3,7 @@ name = "scripts"
|
||||
version = "1.0.0"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"json5>=0.12.0",
|
||||
"pillow==10.2.0",
|
||||
"polib>=1.2.0",
|
||||
"pydantic>=2.11.7",
|
||||
|
11
scripts/uv.lock
generated
11
scripts/uv.lock
generated
@ -11,6 +11,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "json5"
|
||||
version = "0.12.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/12/be/c6c745ec4c4539b25a278b70e29793f10382947df0d9efba2fa09120895d/json5-0.12.0.tar.gz", hash = "sha256:0b4b6ff56801a1c7dc817b0241bca4ce474a0e6a163bfef3fc594d3fd263ff3a", size = 51907, upload-time = "2025-04-03T16:33:13.201Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/41/9f/3500910d5a98549e3098807493851eeef2b89cdd3032227558a104dfe926/json5-0.12.0-py3-none-any.whl", hash = "sha256:6d37aa6c08b0609f16e1ec5ff94697e2cbbfbad5ac112afa05794da9ab7810db", size = 36079, upload-time = "2025-04-03T16:33:11.927Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "10.2.0"
|
||||
@ -135,6 +144,7 @@ name = "scripts"
|
||||
version = "1.0.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "json5" },
|
||||
{ name = "pillow" },
|
||||
{ name = "polib" },
|
||||
{ name = "pydantic" },
|
||||
@ -142,6 +152,7 @@ dependencies = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "json5", specifier = ">=0.12.0" },
|
||||
{ name = "pillow", specifier = "==10.2.0" },
|
||||
{ name = "polib", specifier = ">=1.2.0" },
|
||||
{ name = "pydantic", specifier = ">=2.11.7" },
|
||||
|
@ -1,18 +1,10 @@
|
||||
import json, logging, ast, typing
|
||||
import logging, ast, typing
|
||||
import common, bme
|
||||
from common import AssetKind
|
||||
import pydantic
|
||||
import pydantic, json5
|
||||
|
||||
#region Assistant Checker
|
||||
|
||||
# TODO:
|
||||
# If possible, following check should be done.
|
||||
# They are not done now because they are so complex to implement.
|
||||
# - The reference to variables and functions in programmable fields.
|
||||
# - The return type of prorgammable fields.
|
||||
# - Texture name referred in the programmable field in Face.
|
||||
# - In instance, passed params to instance is fulfilled.
|
||||
|
||||
|
||||
def _try_add(entries: set[str], entry: str) -> bool:
|
||||
if entry in entries:
|
||||
@ -23,9 +15,6 @@ def _try_add(entries: set[str], entry: str) -> bool:
|
||||
|
||||
|
||||
def _check_programmable_field(probe: str) -> None:
|
||||
# TODO:
|
||||
# If possible, allow checking the reference to variables and function,
|
||||
# to make sure the statement must can be executed.
|
||||
try:
|
||||
ast.parse(probe)
|
||||
except SyntaxError:
|
||||
@ -178,7 +167,7 @@ def validate_jsons() -> None:
|
||||
|
||||
# Load all prototypes and check their basic format
|
||||
prototypes: list[bme.Prototype] = []
|
||||
for raw_json_file in raw_jsons_dir.glob('*.json'):
|
||||
for raw_json_file in raw_jsons_dir.glob('*.json5'):
|
||||
# Skip non-file
|
||||
if not raw_json_file.is_file():
|
||||
continue
|
||||
@ -189,12 +178,12 @@ def validate_jsons() -> None:
|
||||
# Load prototypes
|
||||
try:
|
||||
with open(raw_json_file, 'r', encoding='utf-8') as f:
|
||||
docuement = json.load(f)
|
||||
docuement = json5.load(f)
|
||||
file_prototypes = bme.Prototypes.model_validate(docuement)
|
||||
except json.JSONDecodeError as e:
|
||||
logging.error(f'File {raw_json_file} is not a valid JSON file. Reason: {e}')
|
||||
except pydantic.ValidationError as e:
|
||||
logging.error(f'JSON file {raw_json_file} lose essential fields. Detail: {e}')
|
||||
except (ValueError, UnicodeDecodeError) as e:
|
||||
logging.error(f'File {raw_json_file} is not a valid JSON5 file. Reason: {e}')
|
||||
|
||||
# Append all prototypes into list
|
||||
prototypes += file_prototypes.root
|
||||
|
Reference in New Issue
Block a user