20 Commits

Author SHA1 Message Date
2b2b18cfa4 fix: add error report for invalid file path when importing or exporting virtools file.
- add error report for invalid file path when importing or exporting virtools to avoid BMapException was thrown. Reported by SongRui
2025-08-25 14:09:10 +08:00
b19800e37f fix: fix bug that there is no preset encoding names.
- fix the issue that there is no preset encoding names in list when enable plugin without any extra operations.
2025-08-25 13:53:57 +08:00
e14729500c fix: fix rail adders poll issue
- add Ballance Texture requirement for all rail adders because they need it.
2025-08-25 13:24:15 +08:00
48bfc54830 i18n: modify i18n file batchly.
- update translation context in i18n files batchly due to previous BME prototype changes.
2025-08-25 13:13:42 +08:00
7e74e42bd7 feat: add BME category display in blender.
- add BME category display in blender, including add menu and side menu.
2025-08-25 13:07:55 +08:00
96a81b165b feat: add category fields for BME.
- add category for BME prorotypes.
- update validator and extractor for this change.
2025-08-25 10:30:44 +08:00
0681f0d240 fix: optimize the ui layout for BME boolean property. 2025-08-20 14:40:14 +08:00
d700f1276a fix: fix the wrong showcase type of Is Sink field in BME prototype Flat. 2025-08-20 14:32:57 +08:00
3bea3d67b9 feat: add OP_OBJECT_game_view operator.
- add OP_OBJECT_game_view operator for changing blender render resolution to some game resolution presets.
2025-08-18 21:55:15 +08:00
ec41b7553a feat: add game camera feature.
- add game camera operator which allow user see camera view as Ballance game presented.
2025-08-18 16:02:50 +08:00
9e2539499e fix: fix wrong UI words in legacy align. 2025-08-04 13:50:56 +08:00
3a5cd1c937 feat: support macos arm64 arch
- specially thank doyaGu for compiling macos arm64 BMap binary.
2025-08-04 11:22:09 +08:00
1afa5f9893 fix: change the shape of BME floor terminal.
- change the shape of all BME floor terminal (normal, sink and wide) from 5x2.5 to 5x5 requested by Zzq and Lee623.
- change icons according to above changes.
- add some comment in prototype files.
2025-08-01 15:13:38 +08:00
1383e87104 feat: allow 3D Cursor as align source in legacy align operator.
- allow 3D Cursor as align source in legacy align operator. this feature is requested by Zzq.
- add icon for legacy align.
2025-08-01 15:13:21 +08:00
a2b8f41a21 fix: fix performance after adding sidebar panel.
- resolve a performance issue by removing useless feature.
- more details about this issue can be seen the content inside this commit.
2025-07-31 16:50:32 +08:00
93f23abeb9 feat: add Ballance menu in 3d view sidebar for convenient adding. 2025-07-31 12:02:40 +08:00
4ba3ff9e5a fix: fix the aftermath of changing EnumPropHelper.
- fix the generic error of EnumPropHelper.
- use EnumPropHelper in UTIL_ioport_shared.ExportParams field instead of raw Blender string.
- remove useless type hint in various modules.
2025-07-30 13:35:36 +08:00
a9a889a8fd refactor: use generic type in EnumPropHelper
- use typing.Generic in EnumPropHelper and its child classes.
- change Doxygen docstring into reStructedText docstring.
2025-07-30 10:56:24 +08:00
fc34b19a42 feat: allow exporting selected objects as Virtools file
- add Selected Objects option in exporting Virtools file window requested by ZZQ.
2025-07-29 21:43:59 +08:00
9e65d258d7 refactor: use JSON5 instead of JSON for BME prototype.
- use JSON5 for BME prototype description file instead of JSON to make us have ability that make comment in declaration files (TBD in future).
- upgrade corresponding scripts.
- confirm the finish of upgrading script into modern Python.
2025-07-29 21:14:02 +08:00
46 changed files with 1797 additions and 976 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

@ -84,6 +84,7 @@
"identifier": "floor_normal_1x1",
"showcase": {
"title": "Normal 1x1",
"category": "1x1 Blocks",
"icon": "Normal1x1",
"type": "floor",
"cfgs": [
@ -134,6 +135,7 @@
"identifier": "floor_sink_1x1",
"showcase": {
"title": "Sink 1x1",
"category": "1x1 Blocks",
"icon": "Sink1x1",
"type": "floor",
"cfgs": [

View File

@ -49,6 +49,7 @@
"identifier": "floor_normal_border",
"showcase": {
"title": "Normal Border",
"category": "Borders",
"icon": "NormalBorder",
"type": "floor",
"cfgs": [
@ -112,6 +113,7 @@
"identifier": "floor_sink_border",
"showcase": {
"title": "Sink Border",
"category": "Borders",
"icon": "SinkBorder",
"type": "floor",
"cfgs": [
@ -175,6 +177,7 @@
"identifier": "floor_ribbon_border",
"showcase": {
"title": "Ribbon Border",
"category": "Borders",
"icon": "RibbonBorder",
"type": "floor",
"cfgs": [

View File

@ -1,4 +1,12 @@
[
// One of Chris suggested more vanilla prototypes.
// This prototype represent a half of a normal, sink or double ribbon border,
// which looks like trapezoid from top to bottom.
//
// The bottom edge of trapezoid is from origin to +X with `long_edge_length` length.
// The length of top edge is `short_edge_length` and it just like moving bottom edge to +Y direction.
// The offset between top edge and bottom edge is always 2.5.
// The distance from the closest point of top edge, to Y axis is `short_edge_offset`.
{
"identifier": "cv_trapezoid_side",
"showcase": null,
@ -153,6 +161,11 @@
}
]
},
// Same as previous one, but looks like triangle from top to bottom.
//
// The bottom edge is from origin to +X with `edge_length` length.
// The tip is going to +Y.
// The height of this triangle is always 2.5 and the offset between tip and Y axis is `tip_offset`.
{
"identifier": "cv_triangle_side",
"showcase": null,

View File

@ -149,6 +149,7 @@
"identifier": "floor_normal_inner_corner",
"showcase": {
"title": "Normal Inner Corner",
"category": "Half Block Corners",
"icon": "NormalInnerCorner",
"type": "floor",
"cfgs": [
@ -201,6 +202,7 @@
"identifier": "floor_sink_inner_corner",
"showcase": {
"title": "Sink Inner Corner",
"category": "Half Block Corners",
"icon": "SinkInnerCorner",
"type": "floor",
"cfgs": [
@ -253,6 +255,7 @@
"identifier": "floor_ribbon_inner_corner",
"showcase": {
"title": "Ribbon Inner Corner",
"category": "Half Block Corners",
"icon": "RibbonInnerCorner",
"type": "floor",
"cfgs": [
@ -305,6 +308,7 @@
"identifier": "floor_normal_outter_corner",
"showcase": {
"title": "Normal Outter Corner",
"category": "Half Block Corners",
"icon": "NormalOutterCorner",
"type": "floor",
"cfgs": [
@ -357,6 +361,7 @@
"identifier": "floor_sink_outter_corner",
"showcase": {
"title": "Sink Outter Corner",
"category": "Half Block Corners",
"icon": "SinkOutterCorner",
"type": "floor",
"cfgs": [
@ -409,6 +414,7 @@
"identifier": "floor_ribbon_outter_corner",
"showcase": {
"title": "Ribbon Outter Corner",
"category": "Half Block Corners",
"icon": "RibbonOutterCorner",
"type": "floor",
"cfgs": [

View File

@ -228,6 +228,7 @@
"identifier": "floor_normal_l_crossing",
"showcase": {
"title": "Normal L Crossing",
"category": "Floor Crossings",
"icon": "NormalLCrossing",
"type": "floor",
"cfgs": [
@ -278,6 +279,7 @@
"identifier": "floor_sink_l_crossing",
"showcase": {
"title": "Sink L Crossing",
"category": "Floor Crossings",
"icon": "SinkLCrossing",
"type": "floor",
"cfgs": [
@ -328,6 +330,7 @@
"identifier": "floor_normal_t_crossing",
"showcase": {
"title": "Normal T Crossing",
"category": "Floor Crossings",
"icon": "NormalTCrossing",
"type": "floor",
"cfgs": [
@ -378,6 +381,7 @@
"identifier": "floor_sink_t_crossing",
"showcase": {
"title": "Sink T Crossing",
"category": "Floor Crossings",
"icon": "SinkTCrossing",
"type": "floor",
"cfgs": [
@ -428,6 +432,7 @@
"identifier": "floor_normal_x_crossing",
"showcase": {
"title": "Normal X Crossing",
"category": "Floor Crossings",
"icon": "NormalXCrossing",
"type": "floor",
"cfgs": [
@ -478,6 +483,7 @@
"identifier": "floor_sink_x_crossing",
"showcase": {
"title": "Sink X Crossing",
"category": "Floor Crossings",
"icon": "SinkXCrossing",
"type": "floor",
"cfgs": [

View File

@ -3,6 +3,7 @@
"identifier": "floor_flat",
"showcase": {
"title": "Flat",
"category": "Miscellaneous",
"icon": "Flat",
"type": "floor",
"cfgs": [
@ -36,7 +37,7 @@
},
{
"field": "is_sink_",
"type": "float",
"type": "bool",
"title": "Is Sink",
"desc": "Whether this flat floor is used for sink floor.",
"default": "False"

View File

@ -116,6 +116,7 @@
"identifier": "floor_normal_platform",
"showcase": {
"title": "Normal Platform",
"category": "Platforms",
"icon": "NormalPlatform",
"type": "floor",
"cfgs": [
@ -191,6 +192,7 @@
"identifier": "floor_sink_platform",
"showcase": {
"title": "Sink Platform",
"category": "Platforms",
"icon": "SinkPlatform",
"type": "floor",
"cfgs": [
@ -266,6 +268,7 @@
"identifier": "floor_ribbon_platform",
"showcase": {
"title": "Ribbon Platform",
"category": "Platforms",
"icon": "RibbonPlatform",
"type": "floor",
"cfgs": [

View File

@ -3,6 +3,7 @@
"identifier": "floor_normal_straight",
"showcase": {
"title": "Normal Floor",
"category": "Floors",
"icon": "NormalFloor",
"type": "floor",
"cfgs": [
@ -142,6 +143,7 @@
"identifier": "floor_sink_straight",
"showcase": {
"title": "Sink Floor",
"category": "Floors",
"icon": "SinkFloor",
"type": "floor",
"cfgs": [

View File

@ -1,4 +1,5 @@
[
// The shared template prototype used by all floor terminals.
{
"identifier": "raw_floor_terminal",
"showcase": null,
@ -22,26 +23,30 @@
"faces": [],
"instances": [
{
"identifier": "cv_triangle_side",
"identifier": "cv_trapezoid_side",
"skip": "False",
"params": {
"edge_length": "2.5",
"tip_offset": "2.5",
"long_edge_length": "5.0",
"short_edge_offset": "2.5",
"short_edge_length": "2.5",
"height": "height",
"face": "(face[0], False, False, face[3], face[4], None)",
"is_sink": "is_sink"
"face": "(face[0], False, False, face[3], face[4], False)",
"is_sink": "is_sink",
"is_ribbon": "False"
},
"transform": "ident()"
},
{
"identifier": "cv_triangle_side",
"identifier": "cv_trapezoid_side",
"skip": "False",
"params": {
"edge_length": "2.5",
"tip_offset": "2.5",
"long_edge_length": "5.0",
"short_edge_offset": "2.5",
"short_edge_length": "2.5",
"height": "height",
"face": "(face[0], False, False, face[3], face[5], None)",
"is_sink": "is_sink"
"face": "(face[0], False, False, face[3], face[5], False)",
"is_sink": "is_sink",
"is_ribbon": "False"
},
"transform": "move(0, 5, 0) @ scale(1, -1, 1)"
},
@ -61,7 +66,7 @@
"identifier": "floor_rectangle_bottom",
"skip": "not face[1]",
"params": {
"length": "2.5",
"length": "5",
"width": "5"
},
"transform": "move(0, 0, -height)"
@ -72,6 +77,7 @@
"identifier": "floor_normal_terminal",
"showcase": {
"title": "Normal Floor Terminal",
"category": "Floors",
"icon": "NormalFloorTerminal",
"type": "floor",
"cfgs": [
@ -122,6 +128,7 @@
"identifier": "floor_sink_terminal",
"showcase": {
"title": "Sink Floor Terminal",
"category": "Floors",
"icon": "SinkFloorTerminal",
"type": "floor",
"cfgs": [

View File

@ -137,6 +137,7 @@
"identifier": "wood_trafo",
"showcase": {
"title": "Wood Trafo",
"category": "Trafo",
"icon": "WoodTrafo",
"type": "floor",
"cfgs": [
@ -187,6 +188,7 @@
"identifier": "stone_trafo",
"showcase": {
"title": "Stone Trafo",
"category": "Trafo",
"icon": "StoneTrafo",
"type": "floor",
"cfgs": [
@ -237,6 +239,7 @@
"identifier": "paper_trafo",
"showcase": {
"title": "Paper Trafo",
"category": "Trafo",
"icon": "PaperTrafo",
"type": "floor",
"cfgs": [

View File

@ -111,6 +111,7 @@
"identifier": "floor_transition",
"showcase": {
"title": "Transition",
"category": "Miscellaneous",
"icon": "Transition",
"type": "floor",
"cfgs": [
@ -191,6 +192,7 @@
"identifier": "floor_narrow_transition",
"showcase": {
"title": "Narrow Transition",
"category": "Miscellaneous",
"icon": "NarrowTransition",
"type": "floor",
"cfgs": [

View File

@ -3,6 +3,7 @@
"identifier": "floor_wide_straight",
"showcase": {
"title": "Wide Floor",
"category": "Wide Floors",
"icon": "WideFloor",
"type": "floor",
"cfgs": [
@ -106,6 +107,7 @@
"identifier": "floor_wide_terminal",
"showcase": {
"title": "Wide Floor Terminal",
"category": "Wide Floors",
"icon": "WideFloorTerminal",
"type": "floor",
"cfgs": [
@ -166,34 +168,50 @@
"transform": "rot(0, 0, 90) @ scale(1, -1, 1)"
},
{
"identifier": "cv_triangle_side",
"identifier": "cv_trapezoid_side",
"skip": "False",
"params": {
"edge_length": "2.5",
"tip_offset": "2.5",
"long_edge_length": "5.0",
"short_edge_offset": "2.5",
"short_edge_length": "2.5",
"height": "height",
"face": "(face[0], False, False, face[3], face[4], None)",
"is_sink": "True"
"face": "(face[0], False, False, face[3], face[4], False)",
"is_sink": "True",
"is_ribbon": "False"
},
"transform": "ident()"
},
{
"identifier": "cv_triangle_side",
"identifier": "cv_trapezoid_side",
"skip": "False",
"params": {
"edge_length": "2.5",
"tip_offset": "2.5",
"long_edge_length": "5.0",
"short_edge_offset": "2.5",
"short_edge_length": "2.5",
"height": "height",
"face": "(face[0], False, False, face[3], face[5], None)",
"is_sink": "True"
"face": "(face[0], False, False, face[3], face[5], False)",
"is_sink": "True",
"is_ribbon": "False"
},
"transform": "move(0, width + 5, 0) @ scale(1, -1, 1)"
},
{
"identifier": "floor_flat",
"skip": "False",
"params": {
"height": "height",
"length": "2.5",
"width": "width",
"face": "(face[0], False, False, face[3], False, False)",
"is_sink": "True"
},
"transform": "move(2.5, 2.5, 0)"
},
{
"identifier": "floor_rectangle_bottom",
"skip": "not face[1]",
"params": {
"length": "2.5",
"length": "5",
"width": "5 + width"
},
"transform": "move(0, 0, -height)"
@ -204,6 +222,7 @@
"identifier": "floor_wide_l_crossing",
"showcase": {
"title": "Wide Floor L Crossing",
"category": "Wide Floors",
"icon": "WideLCrossing",
"type": "floor",
"cfgs": [
@ -336,6 +355,7 @@
"identifier": "floor_wide_t_crossing",
"showcase": {
"title": "Wide Floor T Crossing",
"category": "Wide Floors",
"icon": "WideTCrossing",
"type": "floor",
"cfgs": [
@ -459,6 +479,7 @@
"identifier": "floor_wide_x_crossing",
"showcase": {
"title": "Wide Floor X Crossing",
"category": "Wide Floors",
"icon": "WideXCrossing",
"type": "floor",
"cfgs": [

View File

@ -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.
#
# 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.
#
# 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
# 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.
#
# 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
#
# ===== 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]
@ -264,7 +250,7 @@ class BBP_OT_add_bme_struct(bpy.types.Operator):
case UTIL_bme.PrototypeShowcaseCfgsTypes.Float:
box_layout.prop(op_cfgs_visitor[cfg_index], 'prop_float', text='')
case UTIL_bme.PrototypeShowcaseCfgsTypes.Boolean:
box_layout.prop(op_cfgs_visitor[cfg_index], 'prop_bool', text='')
box_layout.prop(op_cfgs_visitor[cfg_index], 'prop_bool', toggle=1, text='Yes', text_ctxt='BBP_OT_add_bme_struct/draw')
case UTIL_bme.PrototypeShowcaseCfgsTypes.Face:
# face will show a special layout (grid view)
grids = box_layout.grid_flow(
@ -294,16 +280,24 @@ class BBP_OT_add_bme_struct(bpy.types.Operator):
@classmethod
def draw_blc_menu(cls, layout: bpy.types.UILayout):
for ident in _g_EnumHelper_BmeStructType.get_bme_identifiers():
# draw operator
cop = layout.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)
)
# and assign its init type value
cop.bme_struct_type = _g_EnumHelper_BmeStructType.to_selection(ident)
for category, idents in _g_EnumHelper_BmeStructType.get_bme_categories().items():
# draw category label
layout.label(text=category, text_ctxt=UTIL_translation.build_prototype_showcase_category_context())
# draw prototypes list
for ident in idents:
# draw operator
cop = layout.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_title_context(ident),
)
# and assign its init type value
cop.bme_struct_type = _g_EnumHelper_BmeStructType.to_selection(ident)
# draw separator
layout.separator()
#endregion

View File

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

View File

@ -1,6 +1,6 @@
import bpy, mathutils, math
import typing
from . import UTIL_rail_creator
from . import UTIL_rail_creator, PROP_preferences
## Const Value Hint:
# Default Rail Radius: 0.35 (in measure)
@ -233,6 +233,10 @@ class BBP_OT_add_rail_section(SharedRailSectionInputProperty, bpy.types.Operator
bl_options = {'REGISTER', 'UNDO'}
bl_translation_context = 'BBP_OT_add_rail_section'
@classmethod
def poll(cls, context):
return PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder()
def execute(self, context):
UTIL_rail_creator.rail_creator_wrapper(
lambda bm: UTIL_rail_creator.create_rail_section(
@ -254,6 +258,10 @@ class BBP_OT_add_transition_section(bpy.types.Operator):
bl_options = {'REGISTER', 'UNDO'}
bl_translation_context = 'BBP_OT_add_transition_section'
@classmethod
def poll(cls, context):
return PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder()
def execute(self, context):
UTIL_rail_creator.rail_creator_wrapper(
lambda bm: UTIL_rail_creator.create_transition_section(bm, c_DefaultRailRadius, c_DefaultRailSpan),
@ -272,6 +280,10 @@ class BBP_OT_add_straight_rail(SharedExtraTransform, SharedRailSectionInputPrope
bl_options = {'REGISTER', 'UNDO'}
bl_translation_context = 'BBP_OT_add_straight_rail'
@classmethod
def poll(cls, context):
return PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder()
def execute(self, context):
UTIL_rail_creator.rail_creator_wrapper(
lambda bm: UTIL_rail_creator.create_straight_rail(
@ -301,6 +313,10 @@ class BBP_OT_add_transition_rail(SharedExtraTransform, SharedRailCapInputPropert
bl_options = {'REGISTER', 'UNDO'}
bl_translation_context = 'BBP_OT_add_transition_rail'
@classmethod
def poll(cls, context):
return PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder()
def execute(self, context):
UTIL_rail_creator.rail_creator_wrapper(
lambda bm: UTIL_rail_creator.create_transition_rail(
@ -340,6 +356,10 @@ class BBP_OT_add_side_rail(SharedExtraTransform, SharedRailCapInputProperty, Sha
translation_context = 'BBP_OT_add_side_rail/property'
) # type: ignore
@classmethod
def poll(cls, context):
return PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder()
def execute(self, context):
UTIL_rail_creator.rail_creator_wrapper(
lambda bm: UTIL_rail_creator.create_straight_rail(
@ -379,6 +399,10 @@ class BBP_OT_add_arc_rail(SharedExtraTransform, SharedRailSectionInputProperty,
translation_context = 'BBP_OT_add_arc_rail/property'
) # type: ignore
@classmethod
def poll(cls, context):
return PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder()
def execute(self, context):
UTIL_rail_creator.rail_creator_wrapper(
lambda bm: UTIL_rail_creator.create_screw_rail(
@ -430,6 +454,10 @@ class BBP_OT_add_spiral_rail(SharedExtraTransform, SharedRailCapInputProperty, S
translation_context = 'BBP_OT_add_spiral_rail/property'
) # type: ignore
@classmethod
def poll(cls, context):
return PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder()
def execute(self, context):
UTIL_rail_creator.rail_creator_wrapper(
lambda bm: UTIL_rail_creator.create_screw_rail(
@ -474,6 +502,10 @@ class BBP_OT_add_side_spiral_rail(SharedExtraTransform, SharedRailSectionInputPr
translation_context = 'BBP_OT_add_side_spiral_rail/property'
) # type: ignore
@classmethod
def poll(cls, context):
return PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder()
def execute(self, context):
UTIL_rail_creator.rail_creator_wrapper(
lambda bm: UTIL_rail_creator.create_screw_rail(

View File

@ -18,10 +18,16 @@ class BBP_OT_export_virtools(bpy.types.Operator, UTIL_file_browser.ExportVirtool
return (
PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder()
and bmap.is_bmap_available())
def invoke(self, context, event):
# preset virtools encoding if possible
self.preset_vt_encodings_if_possible(context)
# call parent invoke function (same reason written in IMPORT module)
return super().invoke(context, event)
def execute(self, context):
# check selecting first
objls: tuple[bpy.types.Object] | None = self.general_get_export_objects(context)
objls: tuple[bpy.types.Object, ...] | None = self.general_get_export_objects(context)
if objls is None:
self.report({'ERROR'}, 'No selected target!')
return {'CANCELLED'}
@ -38,10 +44,16 @@ class BBP_OT_export_virtools(bpy.types.Operator, UTIL_file_browser.ExportVirtool
self.report({'ERROR'}, 'You must specify at least one encoding for file saving (e.g. cp1252, gbk)!')
return {'CANCELLED'}
# check file name
filename = self.general_get_filename()
if not os.path.isfile(filename):
self.report({'ERROR'}, 'No file was selected!')
return {'CANCELLED'}
# start exporting
with UTIL_ioport_shared.ExportEditModeBackup() as editmode_guard:
_export_virtools(
self.general_get_filename(),
filename,
encodings,
texture_save_opt,
self.general_get_use_compress(),
@ -68,7 +80,7 @@ _TTexturePair = tuple[bpy.types.Image, bmap.BMTexture]
def _export_virtools(
file_name_: str,
encodings_: tuple[str],
encodings_: tuple[str, ...],
texture_save_opt_: UTIL_virtools_types.CK_TEXTURE_SAVEOPTIONS,
use_compress_: bool,
compress_level_: int,

View File

@ -18,6 +18,12 @@ class BBP_OT_import_virtools(bpy.types.Operator, UTIL_file_browser.ImportVirtool
return (
PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder()
and bmap.is_bmap_available())
def invoke(self, context, event):
# preset virtools encoding if possible
self.preset_vt_encodings_if_possible(context)
# call parent invoke function (do no call self "execute", because we need show a modal window)
return super().invoke(context, event)
def execute(self, context):
# check whether encoding list is empty to avoid real stupid user.
@ -26,8 +32,14 @@ class BBP_OT_import_virtools(bpy.types.Operator, UTIL_file_browser.ImportVirtool
self.report({'ERROR'}, 'You must specify at least one encoding for file loading (e.g. cp1252, gbk)!')
return {'CANCELLED'}
# check file name
filename = self.general_get_filename()
if not os.path.isfile(filename):
self.report({'ERROR'}, 'No file was selected!')
return {'CANCELLED'}
_import_virtools(
self.general_get_filename(),
filename,
encodings,
self.general_get_conflict_resolver()
)
@ -40,7 +52,7 @@ class BBP_OT_import_virtools(bpy.types.Operator, UTIL_file_browser.ImportVirtool
self.draw_virtools_params(context, layout, True)
self.draw_ballance_params(layout, True)
def _import_virtools(file_name_: str, encodings_: tuple[str], resolver: UTIL_ioport_shared.ConflictResolver) -> None:
def _import_virtools(file_name_: str, encodings_: tuple[str, ...], resolver: UTIL_ioport_shared.ConflictResolver) -> None:
# create temp folder
with tempfile.TemporaryDirectory() as vt_temp_folder:
tr_text: str = bpy.app.translations.pgettext_rpt(

View File

@ -0,0 +1,399 @@
import bpy, mathutils
import typing, enum, math
from . import UTIL_functions
# TODO:
# This file should have fully refactor after we finish Virtools Camera import and export,
# because this module is highly rely on it. Current implementation is a compromise.
# There is a list of things to be done:
# - Remove BBP_OT_game_resolution operator, because Virtools Camera will have similar function in panel.
# - Update BBP_OT_game_cameraoperator with Virtools Camera.
#region Game Resolution
class ResolutionKind(enum.IntEnum):
Normal = enum.auto()
Extended = enum.auto()
Widescreen = enum.auto()
Panoramic = enum.auto()
def to_resolution(self) -> tuple[int, int]:
match self:
case ResolutionKind.Normal: return (1024, 768)
case ResolutionKind.Extended: return (1280, 720)
case ResolutionKind.Widescreen: return (1400, 600)
case ResolutionKind.Panoramic: return (2000, 700)
_g_ResolutionKindDesc: dict[ResolutionKind, tuple[str, str]] = {
ResolutionKind.Normal: ("Normal", "Aspect ratio: 4:3."),
ResolutionKind.Extended: ("Extended", "Aspect ratio: 16:9."),
ResolutionKind.Widescreen: ("Widescreen", "Aspect ratio: 7:3."),
ResolutionKind.Panoramic: ("Panoramic", "Aspect ratio: 20:7."),
}
_g_EnumHelper_ResolutionKind = UTIL_functions.EnumPropHelper(
ResolutionKind,
lambda x: str(x.value),
lambda x: ResolutionKind(int(x)),
lambda x: _g_ResolutionKindDesc[x][0],
lambda x: _g_ResolutionKindDesc[x][1],
lambda _: ""
)
class BBP_OT_game_resolution(bpy.types.Operator):
"""Set Blender render resolution to Ballance game"""
bl_idname = "bbp.game_resolution"
bl_label = "Game Resolution"
bl_options = {'REGISTER', 'UNDO'}
bl_translation_context = 'BBP_OT_game_resolution'
resolution_kind: bpy.props.EnumProperty(
name = "Resolution Kind",
description = "The type of preset resolution.",
items = _g_EnumHelper_ResolutionKind.generate_items(),
default = _g_EnumHelper_ResolutionKind.to_selection(ResolutionKind.Normal)
) # type: ignore
def invoke(self, context, event):
return self.execute(context)
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.prop(self, 'resolution_kind')
def execute(self, context):
# fetch resolution
resolution_kind = _g_EnumHelper_ResolutionKind.get_selection(self.resolution_kind)
resolution = resolution_kind.to_resolution()
# setup resolution
render_settings = bpy.context.scene.render
render_settings.resolution_x = resolution[0]
render_settings.resolution_y = resolution[1]
return {'FINISHED'}
#endregion
#region Game Camera
#region Enum Defines
class TargetKind(enum.IntEnum):
Cursor = enum.auto()
ActiveObject = enum.auto()
_g_TargetKindDesc: dict[TargetKind, tuple[str, str, str]] = {
TargetKind.Cursor: ("3D Cursor", "3D cursor is player ball.", "CURSOR"),
TargetKind.ActiveObject: ("Active Object", "The origin point of active object is player ball.", "OBJECT_DATA"),
}
_g_EnumHelper_TargetKind = UTIL_functions.EnumPropHelper(
TargetKind,
lambda x: str(x.value),
lambda x: TargetKind(int(x)),
lambda x: _g_TargetKindDesc[x][0],
lambda x: _g_TargetKindDesc[x][1],
lambda x: _g_TargetKindDesc[x][2],
)
class RotationKind(enum.IntEnum):
Preset = enum.auto()
Custom = enum.auto()
_g_RotationKindDesc: dict[RotationKind, tuple[str, str]] = {
RotationKind.Preset: ("Preset", "8 preset rotation angles usually used in game."),
RotationKind.Custom: ("Custom", "User manually input rotation angle.")
}
_g_EnumHelper_RotationKind = UTIL_functions.EnumPropHelper(
RotationKind,
lambda x: str(x.value),
lambda x: RotationKind(int(x)),
lambda x: _g_RotationKindDesc[x][0],
lambda x: _g_RotationKindDesc[x][1],
lambda _: ""
)
class RotationAngle(enum.IntEnum):
Deg0 = enum.auto()
Deg45 = enum.auto()
Deg90 = enum.auto()
Deg135 = enum.auto()
Deg180 = enum.auto()
Deg225 = enum.auto()
Deg270 = enum.auto()
Deg315 = enum.auto()
def to_degree(self) -> float:
match self:
case RotationAngle.Deg0: return 0
case RotationAngle.Deg45: return 45
case RotationAngle.Deg90: return 90
case RotationAngle.Deg135: return 135
case RotationAngle.Deg180: return 180
case RotationAngle.Deg225: return 225
case RotationAngle.Deg270: return 270
case RotationAngle.Deg315: return 315
def to_radians(self) -> float:
return math.radians(self.to_degree())
_g_RotationAngleDesc: dict[RotationAngle, tuple[str, str]] = {
# TODO: Add axis direction in description after we add Camera support when importing
# (because we only can confirm game camera behavior after that).
RotationAngle.Deg0: ("0 Degree", "0 degree"),
RotationAngle.Deg45: ("45 Degree", "45 degree"),
RotationAngle.Deg90: ("90 Degree", "90 degree"),
RotationAngle.Deg135: ("135 Degree", "135 degree"),
RotationAngle.Deg180: ("180 Degree", "180 degree"),
RotationAngle.Deg225: ("225 Degree", "225 degree"),
RotationAngle.Deg270: ("270 Degree", "270 degree"),
RotationAngle.Deg315: ("315 Degree", "315 degree"),
}
_g_EnumHelper_RotationAngle = UTIL_functions.EnumPropHelper(
RotationAngle,
lambda x: str(x.value),
lambda x: RotationAngle(int(x)),
lambda x: _g_RotationAngleDesc[x][0],
lambda x: _g_RotationAngleDesc[x][1],
lambda _: ""
)
class PerspectiveKind(enum.IntEnum):
Ordinary = enum.auto()
Lift = enum.auto()
EasterEgg = enum.auto()
_g_PerspectiveKindDesc: dict[PerspectiveKind, tuple[str, str]] = {
PerspectiveKind.Ordinary: ("Ordinary", "The default perspective for game camera."),
PerspectiveKind.Lift: ("Lift", "Lifted camera in game for downcast level."),
PerspectiveKind.EasterEgg: ("Easter Egg", "A very close view to player ball in game."),
}
_g_EnumHelper_PerspectiveKind = UTIL_functions.EnumPropHelper(
PerspectiveKind,
lambda x: str(x.value),
lambda x: PerspectiveKind(int(x)),
lambda x: _g_PerspectiveKindDesc[x][0],
lambda x: _g_PerspectiveKindDesc[x][1],
lambda _: ""
)
#endregion
class BBP_OT_game_camera(bpy.types.Operator):
"""Order active camera look at target like Ballance does"""
bl_idname = "bbp.game_camera"
bl_label = "Game Camera"
bl_options = {'REGISTER', 'UNDO'}
bl_translation_context = 'BBP_OT_game_camera'
target_kind: bpy.props.EnumProperty(
name = "Target Kind",
description = "",
items = _g_EnumHelper_TargetKind.generate_items(),
default = _g_EnumHelper_TargetKind.to_selection(TargetKind.Cursor)
) # type: ignore
rotation_kind: bpy.props.EnumProperty(
name = "Rotation Angle Kind",
description = "",
items = _g_EnumHelper_RotationKind.generate_items(),
default = _g_EnumHelper_RotationKind.to_selection(RotationKind.Preset)
) # type: ignore
preset_rotation_angle: bpy.props.EnumProperty(
name = "Preset Rotation Angle",
description = "",
items = _g_EnumHelper_RotationAngle.generate_items(),
default = _g_EnumHelper_RotationAngle.to_selection(RotationAngle.Deg0)
) # type: ignore
custom_rotation_angle: bpy.props.FloatProperty(
name = "Custom Rotation Angle",
description = "The rotation angle of camera relative to 3D Cursor",
subtype = 'ANGLE',
min = 0, max = math.radians(360),
step = 100,
# MARK: What the fuck of the precision?
# I set it to 2 but it doesn't work so I forcely set it to 100.
precision = 100,
) # type: ignore
perspective_kind: bpy.props.EnumProperty(
name = "Rotation Angle Kind",
description = "",
items = _g_EnumHelper_PerspectiveKind.generate_items(),
default = _g_EnumHelper_PerspectiveKind.to_selection(PerspectiveKind.Ordinary)
) # type: ignore
@classmethod
def poll(cls, context):
# find camera object
camera_obj = _find_camera_obj()
if camera_obj is None: return False
# find active object
active_obj = bpy.context.active_object
if active_obj is None: return False
# camera object should not be active object
return camera_obj != active_obj
def invoke(self, context, event):
# order user enter camera view
_enter_camera_view()
# then execute following code
return self.execute(context)
def draw(self, context):
layout = self.layout
# Show target picker
layout.label(text='Target', text_ctxt='BBP_OT_game_camera/draw')
layout.row().prop(self, 'target_kind', expand=True)
# Show rotation angle according to different types.
layout.separator()
layout.label(text='Rotation', text_ctxt='BBP_OT_game_camera/draw')
layout.row().prop(self, 'rotation_kind', expand=True)
rot_kind = _g_EnumHelper_RotationKind.get_selection(self.rotation_kind)
match rot_kind:
case RotationKind.Preset:
layout.prop(self, 'preset_rotation_angle', text='')
case RotationKind.Custom:
layout.prop(self, 'custom_rotation_angle', text='')
# Show perspective kind
layout.separator()
layout.label(text='Perspective', text_ctxt='BBP_OT_game_camera/draw')
layout.row().prop(self, 'perspective_kind', expand=True)
def execute(self, context):
# fetch angle
angle: float
rot_kind = _g_EnumHelper_RotationKind.get_selection(self.rotation_kind)
match rot_kind:
case RotationKind.Preset:
rot_angle = _g_EnumHelper_RotationAngle.get_selection(self.preset_rotation_angle)
angle = rot_angle.to_radians()
case RotationKind.Custom:
angle = float(self.custom_rotation_angle)
# fetch others
camera_obj = typing.cast(bpy.types.Object, _find_camera_obj())
target_kind = _g_EnumHelper_TargetKind.get_selection(self.target_kind)
perspective_kind = _g_EnumHelper_PerspectiveKind.get_selection(self.perspective_kind)
# setup its transform and properties
glob_trans = _fetch_glob_translation(camera_obj, target_kind)
_setup_camera_transform(camera_obj, angle, perspective_kind, glob_trans)
_setup_camera_properties(camera_obj)
# return
return {'FINISHED'}
def _find_3d_view_space() -> bpy.types.SpaceView3D | None:
# get current area
area = bpy.context.area
if area is None: return None
# check whether it is 3d view
if area.type != 'VIEW_3D': return None
# get the active space in area
space = area.spaces.active
if space is None: return None
# okey. cast its type and return
return typing.cast(bpy.types.SpaceView3D, space)
def _enter_camera_view() -> None:
space = _find_3d_view_space()
if space is None: return
region = space.region_3d
if region is None: return
region.view_perspective = 'CAMERA'
def _find_camera_obj() -> bpy.types.Object | None:
space = _find_3d_view_space()
if space is None: return None
return space.camera
def _fetch_glob_translation(camobj: bpy.types.Object, target_kind: TargetKind) -> mathutils.Vector:
# we have checked any bad cases in "poll",
# so we can simply return value in there without any check.
match target_kind:
case TargetKind.Cursor:
return bpy.context.scene.cursor.location
case TargetKind.ActiveObject:
return bpy.context.active_object.location
def _setup_camera_transform(camobj: bpy.types.Object, angle: float, perspective: PerspectiveKind, glob_trans: mathutils.Vector) -> None:
# decide the camera offset with ref point
ingamecam_pos: mathutils.Vector
match perspective:
case PerspectiveKind.Ordinary:
ingamecam_pos = mathutils.Vector((22, 0, 35))
case PerspectiveKind.Lift:
ingamecam_pos = mathutils.Vector((22, 0, 35 + 20))
case PerspectiveKind.EasterEgg:
ingamecam_pos = mathutils.Vector((22, 0, 3.86))
# decide the position of ref point
refpot_pos: mathutils.Vector
match perspective:
case PerspectiveKind.EasterEgg:
refpot_pos = mathutils.Vector((4.4, 0, 0))
case _:
refpot_pos = mathutils.Vector((0, 0, 0))
# perform rotation for both positions
player_rot_mat = mathutils.Matrix.Rotation(angle, 4, 'Z')
ingamecam_pos = ingamecam_pos @ player_rot_mat
refpot_pos = refpot_pos @ player_rot_mat
# calculate the rotation of camera
# YYC MARK:
# Following code are linear algebra required.
#
# We can calulate the direction of camera by simply substracting 2 vector.
# In default, the direction of camera is -Z, up direction is +Y.
# So this computed direction is -Z in new cooredinate system.
# Now we can compute +Z axis in this new coordinate system.
new_z = (ingamecam_pos - refpot_pos)
new_z.normalize()
# For ballance camera, all camera is +Z up.
# So we can use it to compute +X axis in new coordinate system
assistant_y = mathutils.Vector((0, 0, 1))
new_x = typing.cast(mathutils.Vector, assistant_y.cross(new_z))
new_x.normalize()
# now we calc the final axis
new_y = typing.cast(mathutils.Vector, new_z.cross(new_x))
new_y.normalize()
# okey, we conbine them as a matrix
rot_mat = mathutils.Matrix((
(new_x.x, new_y.x, new_z.x, 0),
(new_x.y, new_y.y, new_z.y, 0),
(new_x.z, new_y.z, new_z.z, 0),
(0, 0, 0, 1)
))
# calc the final transform matrix and apply it
trans_mat = mathutils.Matrix.Translation(ingamecam_pos)
glob_trans_mat = mathutils.Matrix.Translation(glob_trans)
camobj.matrix_world = glob_trans_mat @ trans_mat @ rot_mat
def _setup_camera_properties(camobj: bpy.types.Object) -> None:
# fetch camera
camera = typing.cast(bpy.types.Camera, camobj.data)
# set clipping
camera.clip_start = 4
camera.clip_end = 1200
# set FOV
camera.lens_unit = 'FOV'
camera.angle = math.radians(58)
#endregion
def register() -> None:
bpy.utils.register_class(BBP_OT_game_resolution)
bpy.utils.register_class(BBP_OT_game_camera)
def unregister() -> None:
bpy.utils.unregister_class(BBP_OT_game_camera)
bpy.utils.unregister_class(BBP_OT_game_resolution)

View File

@ -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,11 +211,22 @@ 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='Target Objects (Selected Objects)', text_ctxt='BBP_OT_legacy_align/draw')
col.label(text='Current Object', 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 Align Mode', text_ctxt='BBP_OT_legacy_align/draw')
col.prop(entry, "current_align_mode", expand = True)
col.label(text='Target Objects Align Mode', text_ctxt='BBP_OT_legacy_align/draw')
col.prop(entry, "target_align_mode", expand = True)
# show apply button
@ -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
# 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
return True
def _prepare_objects() -> tuple[bpy.types.Object, set[bpy.types.Object]]:
# get current object
current_obj: bpy.types.Object = bpy.context.active_object
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)
# 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)
# 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):

View File

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

View File

@ -195,6 +195,16 @@ class PropsVisitor():
def get_ioport_encodings(self) -> tuple[str, ...]:
encodings = get_ioport_encodings(self.__mAssocScene)
return tuple(i.encoding for i in encodings)
def preset_ioport_encodings(self) -> None:
"""
Set IOPort used encodings list as preset encoding list.
Please note that all old values will be overwritten.
"""
encodings = get_ioport_encodings(self.__mAssocScene)
encodings.clear()
for default_enc in UTIL_virtools_types.g_PyBMapDefaultEncodings:
item = encodings.add()
item.encoding = default_enc
def draw_ioport_encodings(self, layout: bpy.types.UILayout) -> None:
target = get_ptrprop_resolver(self.__mAssocScene)
row = layout.row()
@ -218,24 +228,11 @@ class PropsVisitor():
col.separator()
col.operator(BBP_OT_clear_ioport_encodings.bl_idname, icon='TRASH', text='')
@bpy.app.handlers.persistent
def _ioport_encodings_initializer(file_path: str):
# if we can fetch property, and it is empty after loading file
# we fill it with default value
encodings = get_ioport_encodings(bpy.context.scene)
if len(encodings) == 0:
for default_enc in UTIL_virtools_types.g_PyBMapDefaultEncodings:
item = encodings.add()
item.encoding = default_enc
def register() -> None:
bpy.utils.register_class(BBP_PG_bmap_encoding)
bpy.utils.register_class(BBP_UL_bmap_encoding)
bpy.utils.register_class(BBP_PG_ptrprop_resolver)
# register ioport encodings default value
bpy.app.handlers.load_post.append(_ioport_encodings_initializer)
bpy.utils.register_class(BBP_OT_add_ioport_encodings)
bpy.utils.register_class(BBP_OT_rm_ioport_encodings)
bpy.utils.register_class(BBP_OT_up_ioport_encodings)
@ -253,9 +250,6 @@ def unregister() -> None:
bpy.utils.unregister_class(BBP_OT_rm_ioport_encodings)
bpy.utils.unregister_class(BBP_OT_add_ioport_encodings)
# unregister ioport encodings default value
bpy.app.handlers.load_post.remove(_ioport_encodings_initializer)
bpy.utils.unregister_class(BBP_PG_ptrprop_resolver)
bpy.utils.unregister_class(BBP_UL_bmap_encoding)
bpy.utils.unregister_class(BBP_PG_bmap_encoding)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -24,6 +24,7 @@ TOKEN_IDENTIFIER: str = 'identifier'
TOKEN_SHOWCASE: str = 'showcase'
TOKEN_SHOWCASE_TITLE: str = 'title'
TOKEN_SHOWCASE_CATEGORY: str = 'category'
TOKEN_SHOWCASE_ICON: str = 'icon'
TOKEN_SHOWCASE_TYPE: str = 'type'
TOKEN_SHOWCASE_CFGS: str = 'cfgs'
@ -64,10 +65,10 @@ TOKEN_INSTANCES_TRANSFORM: str = 'transform'
#region Prototype Loader
## The list storing BME prototype.
_g_BMEPrototypes: list[dict[str, typing.Any]] = []
## The dict. Key is prototype identifier. value is the index of prototype in prototype list.
"""The list storing BME prototype."""
_g_BMEPrototypeIndexMap: dict[str, int] = {}
"""The dict. Key is prototype identifier. Value is the index of prototype in prototype list."""
# the core loader
for walk_root, walk_dirs, walk_files in os.walk(os.path.join(os.path.dirname(__file__), 'jsons')):
@ -99,7 +100,7 @@ def _env_fct_angle(x1: float, y1: float, x2: float, y2: float) -> float:
# second, its direction (clockwise is positive) is opposite with blender rotation direction (counter-clockwise is positive).
diff = mathutils.Vector((x2, y2)) - mathutils.Vector((x1, y1))
bld_angle = math.degrees(mathutils.Vector((1,0)).angle_signed(diff, 0))
# flip it first
bld_angle = -bld_angle
# process positove number and negative number respectively
@ -141,7 +142,7 @@ _g_ProgFieldGlobals: dict[str, typing.Any] = {
'rot': lambda x, y, z: mathutils.Matrix.LocRotScale(None, mathutils.Euler((math.radians(x), math.radians(y), math.radians(z)), 'XYZ'), None),
'scale': lambda x, y, z: mathutils.Matrix.LocRotScale(None, None, (x, y, z)),
'ident': lambda: mathutils.Matrix.Identity(4),
# my misc custom functions
'distance': _env_fct_distance,
'angle': _env_fct_angle,
@ -187,15 +188,38 @@ 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.
"""
showcase_identifiers: tuple[str, ...]
showcase_categories: dict[str, tuple[str, ...]]
def __init__(self):
# build cache for showcase identifiers and categories
# prepare cache value
identifiers: list[str] = []
categories: dict[str, list[str]] = {}
# iterate showcase prototypes
for x in filter(lambda x: x[TOKEN_SHOWCASE] is not None, _g_BMEPrototypes):
# fetch identifier and category
identifier = typing.cast(str, x[TOKEN_IDENTIFIER])
category = typing.cast(str, x[TOKEN_SHOWCASE][TOKEN_SHOWCASE_CATEGORY])
# add into identifier list
identifiers.append(identifier)
# add into categories
categories_inner = categories.get(category, None)
if categories_inner is None:
categories_inner = []
categories[category] = categories_inner
categories_inner.append(identifier)
# tuple the result
self.showcase_identifiers = tuple(identifiers)
self.showcase_categories = {k: tuple(v) for k, v in categories.items()}
# init parent class
UTIL_functions.EnumPropHelper.__init__(
self,
super().__init__(
self.get_bme_identifiers(),
lambda x: x,
lambda x: x,
@ -203,17 +227,20 @@ class EnumPropHelper(UTIL_functions.EnumPropHelper):
lambda _: '',
lambda x: self.get_bme_showcase_icon(x)
)
def get_bme_identifiers(self) -> tuple[str, ...]:
"""
Get the identifier of prototype which need to be exposed to user.
Template prototype is not included.
In other words, template prototype is not included.
"""
return tuple(
x[TOKEN_IDENTIFIER] # get identifier
for x in filter(lambda x: x[TOKEN_SHOWCASE] is not None, _g_BMEPrototypes) # filter() to filter no showcase template.
)
return self.showcase_identifiers
def get_bme_categories(self) -> dict[str, tuple[str, ...]]:
"""
Get user-oriented identifier list grouped by category.
"""
return self.showcase_categories
def get_bme_showcase_title(self, ident: str) -> str:
"""
Get BME display title by prototype identifier.
@ -327,14 +354,14 @@ def create_bme_struct(
# create mtl slot remap to help following mesh adding
# because mesh writer do not accept string format mtl slot visiting,
# it only accept int based mtl slot index.
#
#
# Also we build face used mtl slot index at the same time.
# So we do not analyse texture field again when providing face data.
# The result is in `prebuild_face_mtl_idx` and please note it will store all face's mtl index.
# For example: if face 0 is skipped and face 1 is used, the first entry in `prebuild_face_mtl_idx`
# will be the mtl slot index used by face 0, not 1. And its length is equal to the face count.
# However, because face 0 is skipped, so the entry is not used and default set to 0.
#
#
# NOTE: since Python 3.6, the item of builtin dict is ordered by inserting order.
# we rely on this to implement following features.
mtl_remap: dict[str, int] = {}
@ -352,7 +379,7 @@ def create_bme_struct(
# if existing, no need to add into remap
# but we need get its index from remap
prebuild_face_mtl_idx[face_idx] = mtl_remap.get(mtl_name, 0)
# pre-compute vertices data because we may need used later.
# Because if face normal data is null, it mean that we need to compute it
# by given vertices.
@ -367,7 +394,7 @@ def create_bme_struct(
cache_bv = typing.cast(mathutils.Vector, transform @ cache_bv)
# get result
prebuild_vec_data.append((cache_bv.x, cache_bv.y, cache_bv.z))
# Check whether given transform is mirror matrix
# because mirror matrix will reverse triangle indice order.
# If matrix is mirror matrix, we need reverse it again in following procession,

View File

@ -2,37 +2,35 @@ 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().")
if (v < min_val): return min_val
elif (v > max_val): return max_val
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().")
if (v < min_val): return min_val
elif (v > max_val): return max_val
else: return v
@ -41,41 +39,65 @@ 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
for item in message:
layout.label(text=item, translate=False)
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:
# 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.
"""
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 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
if len(objs) == 0: return
# set selection for each object
for obj in objs:
obj.select_set(True)
@ -83,66 +105,79 @@ 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
# if there is no active object, we think it is in object mode
if obj is None: return True
# 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.
"""
# 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 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]
# define class member
__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):
__mCollections: typing.Iterable[_TRawEnum]
__mFctToStr: _TFctToStr[_TRawEnum]
__mFctFromStr: _TFctFromStr[_TRawEnum]
__mFctName: _TFctName[_TRawEnum]
__mFctDesc: _TFctDesc[_TRawEnum]
__mFctIcon: _TFctIcon[_TRawEnum]
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
self.__mFctDesc = fct_desc
self.__mFctIcon = fct_icon
def generate_items(self) -> tuple[tuple[str, str, str, int | str, int], ...]:
"""
Generate a tuple which can be applied to Blender EnumProperty's "items".
@ -152,27 +187,29 @@ class EnumPropHelper():
return tuple(
(
self.__mFctToStr(member), # call to_str as its token.
self.__mFctName(member),
self.__mFctDesc(member),
self.__mFctIcon(member),
self.__mFctName(member),
self.__mFctDesc(member),
self.__mFctIcon(member),
idx # use hardcode index, not the collection member self.
) 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)
@ -183,40 +220,43 @@ class CollectionVisitor(typing.Generic[_TPropertyGroup]):
Blender collcetion property lack essential type hint and document.
So I create a wrapper for my personal use to reduce type hint errors raised by my linter.
"""
__mSrcProp: bpy.types.CollectionProperty
def __init__(self, src_prop: bpy.types.CollectionProperty):
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()
def __len__(self) -> int:
return self.__mSrcProp.__len__()
def __getitem__(self, index: int | str) -> _TPropertyGroup:
@ -238,32 +278,50 @@ _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.
We just want to make sure the resources only can be visited by one "with" context.
So it doesn't matter that we do not use lock before operating something.
"""
__mProtectedObjects: set[_TMutexObject]
def __init__(self):
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)

View File

@ -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(
@ -310,6 +340,14 @@ class VirtoolsParams():
translation_context = 'BBP/UTIL_ioport_shared.VirtoolsParams/property'
) # type: ignore
def preset_vt_encodings_if_possible(self, context: bpy.types.Context):
"""
Set preset value for Virtools Encoding list if there is no value inside it.
"""
ptrprops = PROP_ptrprop_resolver.PropsVisitor(context.scene)
if len(ptrprops.get_ioport_encodings()) == 0:
ptrprops.preset_ioport_encodings()
def draw_virtools_params(self, context: bpy.types.Context, layout: bpy.types.UILayout, is_importer: bool) -> None:
header: bpy.types.UILayout
body: bpy.types.UILayout
@ -334,7 +372,6 @@ class VirtoolsParams():
if self.use_compress:
body.prop(self, 'compress_level')
def general_get_vt_encodings(self, context: bpy.types.Context) -> tuple[str, ...]:
# get from ptrprop resolver then filter empty item
ptrprops = PROP_ptrprop_resolver.PropsVisitor(context.scene)
@ -349,6 +386,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 +428,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

View File

@ -55,14 +55,22 @@ import bpy
CTX_BBP: str = 'BBP'
# The universal translation context prefix for BME module in BBP_NG plugin.
CTX_BBP_BME: str = CTX_BBP + '/BME'
def build_prototype_showcase_context(identifier: str) -> str:
CTX_BBP_BME: str = f'{CTX_BBP}/BME'
CTX_BBP_BME_CATEGORY: str = f'{CTX_BBP_BME}/Category'
CTX_BBP_BME_PROTOTYPE: str = f'{CTX_BBP_BME}/Proto'
def build_prototype_showcase_category_context() -> str:
"""
Build the context for getting the translation for BME prototype showcase category.
@return The context for getting translation.
"""
return CTX_BBP_BME_CATEGORY
def build_prototype_showcase_title_context(identifier: str) -> str:
"""
Build the context for getting the translation for BME prototype showcase title.
@param[in] identifier The identifier of this prototype.
@return The context for getting translation.
"""
return CTX_BBP_BME + '/' + identifier
return f'{CTX_BBP_BME_PROTOTYPE}/{identifier}'
def build_prototype_showcase_cfg_context(identifier: str, cfg_index: int) -> str:
"""
Build the context for getting the translation for BME prototype showcase configuration title or description.
@ -70,7 +78,7 @@ def build_prototype_showcase_cfg_context(identifier: str, cfg_index: int) -> str
@param[in] cfg_index The index of this configuration in this prototype showcase.
@return The context for getting translation.
"""
return CTX_BBP_BME + f'/{identifier}/[{cfg_index}]'
return f'{CTX_BBP_BME_PROTOTYPE}/{identifier}/[{cfg_index}]'
#endregion

View File

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

View File

@ -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
@ -25,32 +23,72 @@ from . import OP_IMPORT_bmfile, OP_EXPORT_bmfile, OP_IMPORT_virtools, OP_EXPORT_
from . import OP_UV_flatten_uv, OP_UV_rail_uv
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
from . import OP_OBJECT_legacy_align, OP_OBJECT_virtools_group, OP_OBJECT_snoop_group_then_to_mesh, OP_OBJECT_naming_convention, OP_OBJECT_game_view
#region Menu
#endregion
# ===== Menu Defines =====
#region Menu and Sidebar Panel
class BBP_MT_View3DMenu(bpy.types.Menu):
"""Ballance 3D related operators"""
bl_idname = "BBP_MT_View3DMenu"
bl_label = "Ballance"
bl_translation_context = 'BBP_MT_View3DMenu'
#region Ballance Adder Menu and Panel
def draw(self, context):
layout = self.layout
layout.label(text='UV', icon='UV', text_ctxt='BBP_MT_View3DMenu/draw')
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.separator()
layout.label(text='Align', icon='SNAP_ON', text_ctxt='BBP_MT_View3DMenu/draw')
layout.operator(OP_OBJECT_legacy_align.BBP_OT_legacy_align.bl_idname)
layout.separator()
layout.label(text='Select', icon='SELECT_SET', text_ctxt='BBP_MT_View3DMenu/draw')
layout.operator(OP_OBJECT_virtools_group.BBP_OT_select_object_by_virtools_group.bl_idname)
layout.separator()
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 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"""
@ -59,8 +97,7 @@ class BBP_MT_AddBmeMenu(bpy.types.Menu):
bl_translation_context = 'BBP_MT_AddBmeMenu'
def draw(self, context):
layout = self.layout
OP_ADDS_bme.BBP_OT_add_bme_struct.draw_blc_menu(layout)
reuse_draw_add_bme(self.layout, DrawTarget.BldMenu)
class BBP_MT_AddRailMenu(bpy.types.Menu):
"""Add Ballance Rail"""
@ -69,54 +106,93 @@ class BBP_MT_AddRailMenu(bpy.types.Menu):
bl_translation_context = 'BBP_MT_AddRailMenu'
def draw(self, context):
layout = self.layout
reuse_draw_add_rail(self.layout, DrawTarget.BldMenu)
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):
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):
layout = self.layout
reuse_draw_add_component(self.layout, DrawTarget.BldMenu)
layout.label(text="Basic Components", text_ctxt='BBP_MT_AddComponentsMenu/draw')
OP_ADDS_component.BBP_OT_add_component.draw_blc_menu(layout)
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"""
bl_idname = "BBP_MT_View3DMenu"
bl_label = "Ballance"
bl_translation_context = 'BBP_MT_View3DMenu'
def draw(self, context):
layout = typing.cast(bpy.types.UILayout, self.layout)
layout.label(text='UV', icon='UV', text_ctxt='BBP_MT_View3DMenu/draw')
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.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.label(text='Align', icon='SNAP_ON', text_ctxt='BBP_MT_View3DMenu/draw')
layout.operator(OP_OBJECT_legacy_align.BBP_OT_legacy_align.bl_idname)
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.label(text='Camera', icon='CAMERA_DATA', text_ctxt='BBP_MT_View3DMenu/draw')
layout.operator(OP_OBJECT_game_view.BBP_OT_game_resolution.bl_idname)
layout.operator(OP_OBJECT_game_view.BBP_OT_game_camera.bl_idname)
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)
layout.label(text='Select', icon='SELECT_SET', text_ctxt='BBP_MT_View3DMenu/draw')
layout.operator(OP_OBJECT_virtools_group.BBP_OT_select_object_by_virtools_group.bl_idname)
layout.separator()
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)
# ===== Menu Drawer =====
#endregion
MenuDrawer_t = typing.Callable[[typing.Any, typing.Any], None]
#region Menu Drawer
TFctMenuDrawer = typing.Callable[[typing.Any, typing.Any], None]
def menu_drawer_import(self, context) -> None:
layout: bpy.types.UILayout = self.layout
@ -154,16 +230,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 +265,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 +277,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:
@ -266,6 +350,7 @@ def register() -> None:
OP_OBJECT_virtools_group.register()
OP_OBJECT_snoop_group_then_to_mesh.register()
OP_OBJECT_naming_convention.register()
OP_OBJECT_game_view.register()
# register other classes
for cls in g_BldClasses:
@ -288,6 +373,7 @@ def unregister() -> None:
bpy.utils.unregister_class(cls)
# unregister modules
OP_OBJECT_game_view.unregister()
OP_OBJECT_naming_convention.unregister()
OP_OBJECT_snoop_group_then_to_mesh.unregister()
OP_OBJECT_virtools_group.unregister()

View File

@ -37,7 +37,7 @@ license = [
# ]
# Optional list of supported platforms. If omitted, the extension will be available in all operating systems.
platforms = ["windows-x64", "linux-x64"]
platforms = ["windows-x64", "linux-x64", "macos-arm64"]
# Supported platforms: "windows-x64", "macos-arm64", "linux-x64", "windows-arm64", "macos-x64"
# Optional: bundle 3rd party Python modules.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -27,6 +27,7 @@ class ShowcaseCfg(BaseModel):
class Showcase(BaseModel):
title: str = Field(frozen=True, strict=True)
category: str = Field(frozen=True, strict=True)
icon: str = Field(frozen=True, strict=True)
type: ShowcaseType = Field(frozen=True)
cfgs: list[ShowcaseCfg] = Field(frozen=True, strict=True)

View File

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

View File

@ -1,4 +1,4 @@
import os, typing, logging, enum
import logging, enum, typing
from pathlib import Path

View File

@ -1,88 +1,104 @@
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.
# If the context string of translation changed, please synchronize it.
CTX_TRANSLATION: str = 'BBP/BME'
CTX_PROTOTYPE: str = f'{CTX_TRANSLATION}/Proto'
CTX_CATEGORY: str = f'{CTX_TRANSLATION}/Category'
def _extract_prototype(prototype: bme.Prototype) -> typing.Iterator[polib.POEntry]:
identifier = prototype.identifier
showcase = prototype.showcase
class JsonsExtractor:
# Show message
logging.info(f'Extracting prototype {identifier}')
po: polib.POFile
"""Extracted PO file"""
categories: set[str]
"""Set for removing duplicated category names"""
# Extract showcase
if showcase is None:
return
def __init__(self) -> None:
# create po file
self.po = polib.POFile()
self.po.metadata = {
'Project-Id-Version': '1.0',
'Report-Msgid-Bugs-To': 'you@example.com',
'POT-Creation-Date': 'YEAR-MO-DA HO:MI+ZONE',
'PO-Revision-Date': 'YEAR-MO-DA HO:MI+ZONE',
'Last-Translator': 'FULL NAME <EMAIL@ADDRESS>',
'Language-Team': 'LANGUAGE <LL@li.org>',
'MIME-Version': '1.0',
'Content-Type': 'text/plain; charset=utf-8',
'Content-Transfer-Encoding': '8bit',
'X-Generator': 'polib',
}
# create category set
self.categories = set()
# Extract showcase title
yield polib.POEntry(msgid=showcase.title, msgstr='', msgctxt=f'{CTX_TRANSLATION}/{identifier}')
# Extract showcase entries
for i, cfg in enumerate(showcase.cfgs):
# extract title and description
yield polib.POEntry(msgid=cfg.title, msgstr='', msgctxt=f'{CTX_TRANSLATION}/{identifier}/[{i}]')
yield polib.POEntry(msgid=cfg.desc, msgstr='', msgctxt=f'{CTX_TRANSLATION}/{identifier}/[{i}]')
def __extract_prototype(self, prototype: bme.Prototype) -> None:
identifier = prototype.identifier
showcase = prototype.showcase
# Show message
logging.info(f'Extracting prototype {identifier}')
def _extract_json(json_file: Path) -> typing.Iterator[polib.POEntry]:
# Show message
logging.info(f'Extracting file {json_file}')
# Extract showcase
if showcase is None:
return
try:
# Read file and convert it into BME struct.
with open(json_file, 'r', encoding='utf-8') as f:
document = json.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.')
# Extract showcase title
self.po.append(polib.POEntry(msgid=showcase.title, msgstr='', msgctxt=f'{CTX_PROTOTYPE}/{identifier}'))
# extract showcase category
if showcase.category not in self.categories:
self.po.append(polib.POEntry(msgid=showcase.category, msgstr='', msgctxt=CTX_CATEGORY))
self.categories.add(showcase.category)
# Extract showcase entries
for i, cfg in enumerate(showcase.cfgs):
# extract title and description
self.po.append(polib.POEntry(msgid=cfg.title, msgstr='', msgctxt=f'{CTX_PROTOTYPE}/{identifier}/[{i}]'))
self.po.append(polib.POEntry(msgid=cfg.desc, msgstr='', msgctxt=f'{CTX_PROTOTYPE}/{identifier}/[{i}]'))
# Output nothing
return itertools.chain.from_iterable(())
def __extract_json(self, json_file: Path) -> None:
# Show message
logging.info(f'Extracting file {json_file}')
try:
# Read file and convert it into BME struct.
with open(json_file, 'r', encoding='utf-8') as f:
document = json5.load(f)
prototypes = bme.Prototypes.model_validate(document)
# Extract translation
for prototype in prototypes.root:
self.__extract_prototype(prototype)
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.')
def extract_jsons() -> None:
raw_jsons_dir = common.get_raw_assets_folder(AssetKind.Jsons)
def extract_jsons(self) -> None:
raw_jsons_dir = common.get_raw_assets_folder(AssetKind.Jsons)
# Create POT content
po = polib.POFile()
po.metadata = {
'Project-Id-Version': '1.0',
'Report-Msgid-Bugs-To': 'you@example.com',
'POT-Creation-Date': 'YEAR-MO-DA HO:MI+ZONE',
'PO-Revision-Date': 'YEAR-MO-DA HO:MI+ZONE',
'Last-Translator': 'FULL NAME <EMAIL@ADDRESS>',
'Language-Team': 'LANGUAGE <LL@li.org>',
'MIME-Version': '1.0',
'Content-Type': 'text/plain; charset=utf-8',
'Content-Transfer-Encoding': '8bit',
'X-Generator': 'polib',
}
# Iterate all prototypes and add into POT
for raw_json_file in raw_jsons_dir.glob('*.json5'):
# Skip non-file.
if not raw_json_file.is_file():
continue
# Extract json
self.__extract_json(raw_json_file)
# Iterate all prototypes and add into POT
for raw_json_file in raw_jsons_dir.glob('*.json'):
# Skip non-file.
if not raw_json_file.is_file():
continue
# Extract json and append it.
po.extend(_extract_json(raw_json_file))
# Write into POT file
pot_file = common.get_root_folder() / 'i18n' / 'bme.pot'
logging.info(f'Saving POT into {pot_file}')
po.save(str(pot_file))
def save(self) -> None:
"""Save extracted POT file into correct path"""
pot_file = common.get_root_folder() / 'i18n' / 'bme.pot'
logging.info(f'Saving POT into {pot_file}')
self.po.save(str(pot_file))
if __name__ == '__main__':
common.setup_logging()
extract_jsons()
extractor = JsonsExtractor()
extractor.extract_jsons()
extractor.save()

View File

@ -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
View File

@ -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" },

View File

@ -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:
@ -59,6 +48,9 @@ def _validate_showcase(showcase: bme.Showcase, variables: set[str]) -> None:
# The title of showcase should not be empty
if len(showcase.title) == 0:
logging.error('The title of showcase should not be empty.')
# Category words should not be empty.
if len(showcase.category) == 0:
logging.error('The category of showcase should not be empty.')
# Check icon name
_check_showcase_icon(showcase.icon)
@ -178,7 +170,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 +181,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