14 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
32 changed files with 1178 additions and 583 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", "identifier": "floor_normal_1x1",
"showcase": { "showcase": {
"title": "Normal 1x1", "title": "Normal 1x1",
"category": "1x1 Blocks",
"icon": "Normal1x1", "icon": "Normal1x1",
"type": "floor", "type": "floor",
"cfgs": [ "cfgs": [
@ -134,6 +135,7 @@
"identifier": "floor_sink_1x1", "identifier": "floor_sink_1x1",
"showcase": { "showcase": {
"title": "Sink 1x1", "title": "Sink 1x1",
"category": "1x1 Blocks",
"icon": "Sink1x1", "icon": "Sink1x1",
"type": "floor", "type": "floor",
"cfgs": [ "cfgs": [

View File

@ -49,6 +49,7 @@
"identifier": "floor_normal_border", "identifier": "floor_normal_border",
"showcase": { "showcase": {
"title": "Normal Border", "title": "Normal Border",
"category": "Borders",
"icon": "NormalBorder", "icon": "NormalBorder",
"type": "floor", "type": "floor",
"cfgs": [ "cfgs": [
@ -112,6 +113,7 @@
"identifier": "floor_sink_border", "identifier": "floor_sink_border",
"showcase": { "showcase": {
"title": "Sink Border", "title": "Sink Border",
"category": "Borders",
"icon": "SinkBorder", "icon": "SinkBorder",
"type": "floor", "type": "floor",
"cfgs": [ "cfgs": [
@ -175,6 +177,7 @@
"identifier": "floor_ribbon_border", "identifier": "floor_ribbon_border",
"showcase": { "showcase": {
"title": "Ribbon Border", "title": "Ribbon Border",
"category": "Borders",
"icon": "RibbonBorder", "icon": "RibbonBorder",
"type": "floor", "type": "floor",
"cfgs": [ "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", "identifier": "cv_trapezoid_side",
"showcase": null, "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", "identifier": "cv_triangle_side",
"showcase": null, "showcase": null,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -250,7 +250,7 @@ class BBP_OT_add_bme_struct(bpy.types.Operator):
case UTIL_bme.PrototypeShowcaseCfgsTypes.Float: case UTIL_bme.PrototypeShowcaseCfgsTypes.Float:
box_layout.prop(op_cfgs_visitor[cfg_index], 'prop_float', text='') box_layout.prop(op_cfgs_visitor[cfg_index], 'prop_float', text='')
case UTIL_bme.PrototypeShowcaseCfgsTypes.Boolean: 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: case UTIL_bme.PrototypeShowcaseCfgsTypes.Face:
# face will show a special layout (grid view) # face will show a special layout (grid view)
grids = box_layout.grid_flow( grids = box_layout.grid_flow(
@ -280,17 +280,25 @@ class BBP_OT_add_bme_struct(bpy.types.Operator):
@classmethod @classmethod
def draw_blc_menu(cls, layout: bpy.types.UILayout): def draw_blc_menu(cls, layout: bpy.types.UILayout):
for ident in _g_EnumHelper_BmeStructType.get_bme_identifiers(): 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 # draw operator
cop = layout.operator( cop = layout.operator(
cls.bl_idname, cls.bl_idname,
text = _g_EnumHelper_BmeStructType.get_bme_showcase_title(ident), text = _g_EnumHelper_BmeStructType.get_bme_showcase_title(ident),
icon_value = _g_EnumHelper_BmeStructType.get_bme_showcase_icon(ident), icon_value = _g_EnumHelper_BmeStructType.get_bme_showcase_icon(ident),
text_ctxt = UTIL_translation.build_prototype_showcase_context(ident), text_ctxt = UTIL_translation.build_prototype_showcase_title_context(ident),
) )
# and assign its init type value # and assign its init type value
cop.bme_struct_type = _g_EnumHelper_BmeStructType.to_selection(ident) cop.bme_struct_type = _g_EnumHelper_BmeStructType.to_selection(ident)
# draw separator
layout.separator()
#endregion #endregion
def register() -> None: def register() -> None:

View File

@ -1,6 +1,6 @@
import bpy, mathutils, math import bpy, mathutils, math
import typing import typing
from . import UTIL_rail_creator from . import UTIL_rail_creator, PROP_preferences
## Const Value Hint: ## Const Value Hint:
# Default Rail Radius: 0.35 (in measure) # 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_options = {'REGISTER', 'UNDO'}
bl_translation_context = 'BBP_OT_add_rail_section' 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): def execute(self, context):
UTIL_rail_creator.rail_creator_wrapper( UTIL_rail_creator.rail_creator_wrapper(
lambda bm: UTIL_rail_creator.create_rail_section( 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_options = {'REGISTER', 'UNDO'}
bl_translation_context = 'BBP_OT_add_transition_section' 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): def execute(self, context):
UTIL_rail_creator.rail_creator_wrapper( UTIL_rail_creator.rail_creator_wrapper(
lambda bm: UTIL_rail_creator.create_transition_section(bm, c_DefaultRailRadius, c_DefaultRailSpan), 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_options = {'REGISTER', 'UNDO'}
bl_translation_context = 'BBP_OT_add_straight_rail' 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): def execute(self, context):
UTIL_rail_creator.rail_creator_wrapper( UTIL_rail_creator.rail_creator_wrapper(
lambda bm: UTIL_rail_creator.create_straight_rail( 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_options = {'REGISTER', 'UNDO'}
bl_translation_context = 'BBP_OT_add_transition_rail' 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): def execute(self, context):
UTIL_rail_creator.rail_creator_wrapper( UTIL_rail_creator.rail_creator_wrapper(
lambda bm: UTIL_rail_creator.create_transition_rail( 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' translation_context = 'BBP_OT_add_side_rail/property'
) # type: ignore ) # type: ignore
@classmethod
def poll(cls, context):
return PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder()
def execute(self, context): def execute(self, context):
UTIL_rail_creator.rail_creator_wrapper( UTIL_rail_creator.rail_creator_wrapper(
lambda bm: UTIL_rail_creator.create_straight_rail( 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' translation_context = 'BBP_OT_add_arc_rail/property'
) # type: ignore ) # type: ignore
@classmethod
def poll(cls, context):
return PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder()
def execute(self, context): def execute(self, context):
UTIL_rail_creator.rail_creator_wrapper( UTIL_rail_creator.rail_creator_wrapper(
lambda bm: UTIL_rail_creator.create_screw_rail( 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' translation_context = 'BBP_OT_add_spiral_rail/property'
) # type: ignore ) # type: ignore
@classmethod
def poll(cls, context):
return PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder()
def execute(self, context): def execute(self, context):
UTIL_rail_creator.rail_creator_wrapper( UTIL_rail_creator.rail_creator_wrapper(
lambda bm: UTIL_rail_creator.create_screw_rail( 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' translation_context = 'BBP_OT_add_side_spiral_rail/property'
) # type: ignore ) # type: ignore
@classmethod
def poll(cls, context):
return PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder()
def execute(self, context): def execute(self, context):
UTIL_rail_creator.rail_creator_wrapper( UTIL_rail_creator.rail_creator_wrapper(
lambda bm: UTIL_rail_creator.create_screw_rail( lambda bm: UTIL_rail_creator.create_screw_rail(

View File

@ -19,9 +19,15 @@ class BBP_OT_export_virtools(bpy.types.Operator, UTIL_file_browser.ExportVirtool
PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder() PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder()
and bmap.is_bmap_available()) 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): def execute(self, context):
# check selecting first # 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: if objls is None:
self.report({'ERROR'}, 'No selected target!') self.report({'ERROR'}, 'No selected target!')
return {'CANCELLED'} 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)!') self.report({'ERROR'}, 'You must specify at least one encoding for file saving (e.g. cp1252, gbk)!')
return {'CANCELLED'} 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 # start exporting
with UTIL_ioport_shared.ExportEditModeBackup() as editmode_guard: with UTIL_ioport_shared.ExportEditModeBackup() as editmode_guard:
_export_virtools( _export_virtools(
self.general_get_filename(), filename,
encodings, encodings,
texture_save_opt, texture_save_opt,
self.general_get_use_compress(), self.general_get_use_compress(),
@ -68,7 +80,7 @@ _TTexturePair = tuple[bpy.types.Image, bmap.BMTexture]
def _export_virtools( def _export_virtools(
file_name_: str, file_name_: str,
encodings_: tuple[str], encodings_: tuple[str, ...],
texture_save_opt_: UTIL_virtools_types.CK_TEXTURE_SAVEOPTIONS, texture_save_opt_: UTIL_virtools_types.CK_TEXTURE_SAVEOPTIONS,
use_compress_: bool, use_compress_: bool,
compress_level_: int, compress_level_: int,

View File

@ -19,6 +19,12 @@ class BBP_OT_import_virtools(bpy.types.Operator, UTIL_file_browser.ImportVirtool
PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder() PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder()
and bmap.is_bmap_available()) 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): def execute(self, context):
# check whether encoding list is empty to avoid real stupid user. # check whether encoding list is empty to avoid real stupid user.
encodings = self.general_get_vt_encodings(context) encodings = self.general_get_vt_encodings(context)
@ -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)!') self.report({'ERROR'}, 'You must specify at least one encoding for file loading (e.g. cp1252, gbk)!')
return {'CANCELLED'} 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( _import_virtools(
self.general_get_filename(), filename,
encodings, encodings,
self.general_get_conflict_resolver() 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_virtools_params(context, layout, True)
self.draw_ballance_params(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 # create temp folder
with tempfile.TemporaryDirectory() as vt_temp_folder: with tempfile.TemporaryDirectory() as vt_temp_folder:
tr_text: str = bpy.app.translations.pgettext_rpt( 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

@ -213,7 +213,7 @@ class BBP_OT_legacy_align(bpy.types.Operator):
# show current instance # show current instance
col.separator() col.separator()
col.label(text='Current Instance', 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 # it should be shown in horizon so we create a new sublayout
row = col.row() row = col.row()
row.prop(entry, 'current_instance', expand=True) row.prop(entry, 'current_instance', expand=True)
@ -224,9 +224,9 @@ class BBP_OT_legacy_align(bpy.types.Operator):
# because there is no mode for 3d cursor. # because there is no mode for 3d cursor.
current_instnce = _g_EnumHelper_CurrentInstance.get_selection(entry.current_instance) current_instnce = _g_EnumHelper_CurrentInstance.get_selection(entry.current_instance)
if current_instnce == CurrentInstance.ActiveObject: if current_instnce == CurrentInstance.ActiveObject:
col.label(text='Current Object (Active Object)', text_ctxt='BBP_OT_legacy_align/draw') col.label(text='Current Object Align Mode', text_ctxt='BBP_OT_legacy_align/draw')
col.prop(entry, "current_align_mode", expand = True) 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='Target Objects Align Mode', text_ctxt='BBP_OT_legacy_align/draw')
col.prop(entry, "target_align_mode", expand = True) col.prop(entry, "target_align_mode", expand = True)
# show apply button # show apply button

View File

@ -195,6 +195,16 @@ class PropsVisitor():
def get_ioport_encodings(self) -> tuple[str, ...]: def get_ioport_encodings(self) -> tuple[str, ...]:
encodings = get_ioport_encodings(self.__mAssocScene) encodings = get_ioport_encodings(self.__mAssocScene)
return tuple(i.encoding for i in encodings) 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: def draw_ioport_encodings(self, layout: bpy.types.UILayout) -> None:
target = get_ptrprop_resolver(self.__mAssocScene) target = get_ptrprop_resolver(self.__mAssocScene)
row = layout.row() row = layout.row()
@ -218,24 +228,11 @@ class PropsVisitor():
col.separator() col.separator()
col.operator(BBP_OT_clear_ioport_encodings.bl_idname, icon='TRASH', text='') 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: def register() -> None:
bpy.utils.register_class(BBP_PG_bmap_encoding) bpy.utils.register_class(BBP_PG_bmap_encoding)
bpy.utils.register_class(BBP_UL_bmap_encoding) bpy.utils.register_class(BBP_UL_bmap_encoding)
bpy.utils.register_class(BBP_PG_ptrprop_resolver) 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_add_ioport_encodings)
bpy.utils.register_class(BBP_OT_rm_ioport_encodings) bpy.utils.register_class(BBP_OT_rm_ioport_encodings)
bpy.utils.register_class(BBP_OT_up_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_rm_ioport_encodings)
bpy.utils.unregister_class(BBP_OT_add_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_PG_ptrprop_resolver)
bpy.utils.unregister_class(BBP_UL_bmap_encoding) bpy.utils.unregister_class(BBP_UL_bmap_encoding)
bpy.utils.unregister_class(BBP_PG_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: str = 'showcase'
TOKEN_SHOWCASE_TITLE: str = 'title' TOKEN_SHOWCASE_TITLE: str = 'title'
TOKEN_SHOWCASE_CATEGORY: str = 'category'
TOKEN_SHOWCASE_ICON: str = 'icon' TOKEN_SHOWCASE_ICON: str = 'icon'
TOKEN_SHOWCASE_TYPE: str = 'type' TOKEN_SHOWCASE_TYPE: str = 'type'
TOKEN_SHOWCASE_CFGS: str = 'cfgs' TOKEN_SHOWCASE_CFGS: str = 'cfgs'
@ -64,10 +65,10 @@ TOKEN_INSTANCES_TRANSFORM: str = 'transform'
#region Prototype Loader #region Prototype Loader
## The list storing BME prototype.
_g_BMEPrototypes: list[dict[str, typing.Any]] = [] _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] = {} _g_BMEPrototypeIndexMap: dict[str, int] = {}
"""The dict. Key is prototype identifier. Value is the index of prototype in prototype list."""
# the core loader # the core loader
for walk_root, walk_dirs, walk_files in os.walk(os.path.join(os.path.dirname(__file__), 'jsons')): for walk_root, walk_dirs, walk_files in os.walk(os.path.join(os.path.dirname(__file__), 'jsons')):
@ -192,7 +193,31 @@ class EnumPropHelper(UTIL_functions.EnumPropHelper[str]):
The BME specialized Blender EnumProperty helper. The BME specialized Blender EnumProperty helper.
""" """
showcase_identifiers: tuple[str, ...]
showcase_categories: dict[str, tuple[str, ...]]
def __init__(self): 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 # init parent class
super().__init__( super().__init__(
self.get_bme_identifiers(), self.get_bme_identifiers(),
@ -206,12 +231,15 @@ class EnumPropHelper(UTIL_functions.EnumPropHelper[str]):
def get_bme_identifiers(self) -> tuple[str, ...]: def get_bme_identifiers(self) -> tuple[str, ...]:
""" """
Get the identifier of prototype which need to be exposed to user. 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( return self.showcase_identifiers
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. 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: def get_bme_showcase_title(self, ident: str) -> str:
""" """

View File

@ -340,6 +340,14 @@ class VirtoolsParams():
translation_context = 'BBP/UTIL_ioport_shared.VirtoolsParams/property' translation_context = 'BBP/UTIL_ioport_shared.VirtoolsParams/property'
) # type: ignore ) # 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: def draw_virtools_params(self, context: bpy.types.Context, layout: bpy.types.UILayout, is_importer: bool) -> None:
header: bpy.types.UILayout header: bpy.types.UILayout
body: bpy.types.UILayout body: bpy.types.UILayout
@ -364,7 +372,6 @@ class VirtoolsParams():
if self.use_compress: if self.use_compress:
body.prop(self, 'compress_level') body.prop(self, 'compress_level')
def general_get_vt_encodings(self, context: bpy.types.Context) -> tuple[str, ...]: def general_get_vt_encodings(self, context: bpy.types.Context) -> tuple[str, ...]:
# get from ptrprop resolver then filter empty item # get from ptrprop resolver then filter empty item
ptrprops = PROP_ptrprop_resolver.PropsVisitor(context.scene) ptrprops = PROP_ptrprop_resolver.PropsVisitor(context.scene)

View File

@ -55,14 +55,22 @@ import bpy
CTX_BBP: str = 'BBP' CTX_BBP: str = 'BBP'
# The universal translation context prefix for BME module in BBP_NG plugin. # The universal translation context prefix for BME module in BBP_NG plugin.
CTX_BBP_BME: str = CTX_BBP + '/BME' CTX_BBP_BME: str = f'{CTX_BBP}/BME'
def build_prototype_showcase_context(identifier: str) -> str: 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. Build the context for getting the translation for BME prototype showcase title.
@param[in] identifier The identifier of this prototype. @param[in] identifier The identifier of this prototype.
@return The context for getting translation. @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: 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. 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. @param[in] cfg_index The index of this configuration in this prototype showcase.
@return The context for getting translation. @return The context for getting translation.
""" """
return CTX_BBP_BME + f'/{identifier}/[{cfg_index}]' return f'{CTX_BBP_BME_PROTOTYPE}/{identifier}/[{cfg_index}]'
#endregion #endregion

View File

@ -23,7 +23,7 @@ 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_UV_flatten_uv, OP_UV_rail_uv
from . import OP_MTL_fix_materials from . import OP_MTL_fix_materials
from . import OP_ADDS_component, OP_ADDS_bme, OP_ADDS_rail 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
#endregion #endregion
@ -170,7 +170,7 @@ class BBP_MT_View3DMenu(bpy.types.Menu):
bl_translation_context = 'BBP_MT_View3DMenu' bl_translation_context = 'BBP_MT_View3DMenu'
def draw(self, context): def draw(self, context):
layout = self.layout layout = typing.cast(bpy.types.UILayout, self.layout)
layout.label(text='UV', icon='UV', text_ctxt='BBP_MT_View3DMenu/draw') 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_flatten_uv.BBP_OT_flatten_uv.bl_idname)
layout.operator(OP_UV_rail_uv.BBP_OT_rail_uv.bl_idname) layout.operator(OP_UV_rail_uv.BBP_OT_rail_uv.bl_idname)
@ -178,6 +178,10 @@ class BBP_MT_View3DMenu(bpy.types.Menu):
layout.label(text='Align', icon='SNAP_ON', text_ctxt='BBP_MT_View3DMenu/draw') 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.operator(OP_OBJECT_legacy_align.BBP_OT_legacy_align.bl_idname)
layout.separator() layout.separator()
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='Select', icon='SELECT_SET', text_ctxt='BBP_MT_View3DMenu/draw') 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.operator(OP_OBJECT_virtools_group.BBP_OT_select_object_by_virtools_group.bl_idname)
layout.separator() layout.separator()
@ -346,6 +350,7 @@ def register() -> None:
OP_OBJECT_virtools_group.register() OP_OBJECT_virtools_group.register()
OP_OBJECT_snoop_group_then_to_mesh.register() OP_OBJECT_snoop_group_then_to_mesh.register()
OP_OBJECT_naming_convention.register() OP_OBJECT_naming_convention.register()
OP_OBJECT_game_view.register()
# register other classes # register other classes
for cls in g_BldClasses: for cls in g_BldClasses:
@ -368,6 +373,7 @@ def unregister() -> None:
bpy.utils.unregister_class(cls) bpy.utils.unregister_class(cls)
# unregister modules # unregister modules
OP_OBJECT_game_view.unregister()
OP_OBJECT_naming_convention.unregister() OP_OBJECT_naming_convention.unregister()
OP_OBJECT_snoop_group_then_to_mesh.unregister() OP_OBJECT_snoop_group_then_to_mesh.unregister()
OP_OBJECT_virtools_group.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. # 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" # Supported platforms: "windows-x64", "macos-arm64", "linux-x64", "windows-arm64", "macos-x64"
# Optional: bundle 3rd party Python modules. # 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): class Showcase(BaseModel):
title: str = Field(frozen=True, strict=True) title: str = Field(frozen=True, strict=True)
category: str = Field(frozen=True, strict=True)
icon: str = Field(frozen=True, strict=True) icon: str = Field(frozen=True, strict=True)
type: ShowcaseType = Field(frozen=True) type: ShowcaseType = Field(frozen=True)
cfgs: list[ShowcaseCfg] = Field(frozen=True, strict=True) cfgs: list[ShowcaseCfg] = Field(frozen=True, strict=True)

View File

@ -9,54 +9,21 @@ import pydantic, polib, json5
# If the context string of translation changed, please synchronize it. # If the context string of translation changed, please synchronize it.
CTX_TRANSLATION: str = 'BBP/BME' 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]: class JsonsExtractor:
identifier = prototype.identifier
showcase = prototype.showcase
# Show message po: polib.POFile
logging.info(f'Extracting prototype {identifier}') """Extracted PO file"""
categories: set[str]
"""Set for removing duplicated category names"""
# Extract showcase def __init__(self) -> None:
if showcase is None: # create po file
return self.po = polib.POFile()
self.po.metadata = {
# 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_json(json_file: Path) -> typing.Iterator[polib.POEntry]:
# 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
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.')
# Output nothing
return itertools.chain.from_iterable(())
def extract_jsons() -> None:
raw_jsons_dir = common.get_raw_assets_folder(AssetKind.Jsons)
# Create POT content
po = polib.POFile()
po.metadata = {
'Project-Id-Version': '1.0', 'Project-Id-Version': '1.0',
'Report-Msgid-Bugs-To': 'you@example.com', 'Report-Msgid-Bugs-To': 'you@example.com',
'POT-Creation-Date': 'YEAR-MO-DA HO:MI+ZONE', 'POT-Creation-Date': 'YEAR-MO-DA HO:MI+ZONE',
@ -68,21 +35,70 @@ def extract_jsons() -> None:
'Content-Transfer-Encoding': '8bit', 'Content-Transfer-Encoding': '8bit',
'X-Generator': 'polib', 'X-Generator': 'polib',
} }
# create category set
self.categories = set()
def __extract_prototype(self, prototype: bme.Prototype) -> None:
identifier = prototype.identifier
showcase = prototype.showcase
# Show message
logging.info(f'Extracting prototype {identifier}')
# Extract showcase
if showcase is None:
return
# 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}]'))
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(self) -> None:
raw_jsons_dir = common.get_raw_assets_folder(AssetKind.Jsons)
# Iterate all prototypes and add into POT # Iterate all prototypes and add into POT
for raw_json_file in raw_jsons_dir.glob('*.json5'): for raw_json_file in raw_jsons_dir.glob('*.json5'):
# Skip non-file. # Skip non-file.
if not raw_json_file.is_file(): if not raw_json_file.is_file():
continue continue
# Extract json and append it. # Extract json
po.extend(_extract_json(raw_json_file)) self.__extract_json(raw_json_file)
# Write into POT file def save(self) -> None:
"""Save extracted POT file into correct path"""
pot_file = common.get_root_folder() / 'i18n' / 'bme.pot' pot_file = common.get_root_folder() / 'i18n' / 'bme.pot'
logging.info(f'Saving POT into {pot_file}') logging.info(f'Saving POT into {pot_file}')
po.save(str(pot_file)) self.po.save(str(pot_file))
if __name__ == '__main__': if __name__ == '__main__':
common.setup_logging() 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 # The title of showcase should not be empty
if len(showcase.title) == 0: if len(showcase.title) == 0:
logging.error('The title of showcase should not be empty.') 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 icon name
_check_showcase_icon(showcase.icon) _check_showcase_icon(showcase.icon)