9 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
25 changed files with 761 additions and 561 deletions

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

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

@ -77,6 +77,7 @@
"identifier": "floor_normal_terminal",
"showcase": {
"title": "Normal Floor Terminal",
"category": "Floors",
"icon": "NormalFloorTerminal",
"type": "floor",
"cfgs": [
@ -127,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": [
@ -220,6 +222,7 @@
"identifier": "floor_wide_l_crossing",
"showcase": {
"title": "Wide Floor L Crossing",
"category": "Wide Floors",
"icon": "WideLCrossing",
"type": "floor",
"cfgs": [
@ -352,6 +355,7 @@
"identifier": "floor_wide_t_crossing",
"showcase": {
"title": "Wide Floor T Crossing",
"category": "Wide Floors",
"icon": "WideTCrossing",
"type": "floor",
"cfgs": [
@ -475,6 +479,7 @@
"identifier": "floor_wide_x_crossing",
"showcase": {
"title": "Wide Floor X Crossing",
"category": "Wide Floors",
"icon": "WideXCrossing",
"type": "floor",
"cfgs": [

View File

@ -250,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(
@ -280,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

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

@ -2,10 +2,42 @@ 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
# TODO: this is a temporary solution.
# We use Virtools Camera data replace this once we finish it.
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"""
@ -14,13 +46,29 @@ class BBP_OT_game_resolution(bpy.types.Operator):
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
@ -126,8 +174,6 @@ _g_EnumHelper_PerspectiveKind = UTIL_functions.EnumPropHelper(
#endregion
# TODO: update this operator after we finish Camera import and export.
class BBP_OT_game_camera(bpy.types.Operator):
"""Order active camera look at target like Ballance does"""
bl_idname = "bbp.game_camera"
@ -331,8 +377,6 @@ def _setup_camera_transform(camobj: bpy.types.Object, angle: float, perspective:
camobj.matrix_world = glob_trans_mat @ trans_mat @ rot_mat
def _setup_camera_properties(camobj: bpy.types.Object) -> None:
# TODO: use Virtools Camera replace these in future
# fetch camera
camera = typing.cast(bpy.types.Camera, camobj.data)

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

@ -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,
@ -191,8 +192,32 @@ 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
super().__init__(
self.get_bme_identifiers(),
@ -202,17 +227,20 @@ class EnumPropHelper(UTIL_functions.EnumPropHelper[str]):
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.
@ -326,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] = {}
@ -351,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.
@ -366,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

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

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

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

@ -9,80 +9,96 @@ import pydantic, polib, json5
# 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 = json5.load(f)
prototypes = bme.Prototypes.model_validate(document)
# Extract translation
return itertools.chain.from_iterable(_extract_prototype(prototype) for prototype in prototypes.root)
except 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.')
# 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('*.json5'):
# 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

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