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 1278 additions and 624 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,16 +280,24 @@ 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 operator # draw category label
cop = layout.operator( layout.label(text=category, text_ctxt=UTIL_translation.build_prototype_showcase_category_context())
cls.bl_idname,
text = _g_EnumHelper_BmeStructType.get_bme_showcase_title(ident), # draw prototypes list
icon_value = _g_EnumHelper_BmeStructType.get_bme_showcase_icon(ident), for ident in idents:
text_ctxt = UTIL_translation.build_prototype_showcase_context(ident), # draw operator
) cop = layout.operator(
# and assign its init type value cls.bl_idname,
cop.bme_struct_type = _g_EnumHelper_BmeStructType.to_selection(ident) 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 #endregion

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

@ -18,10 +18,16 @@ class BBP_OT_export_virtools(bpy.types.Operator, UTIL_file_browser.ExportVirtool
return ( return (
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

@ -18,6 +18,12 @@ class BBP_OT_import_virtools(bpy.types.Operator, UTIL_file_browser.ImportVirtool
return ( return (
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.
@ -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

@ -9,11 +9,11 @@ class AlignMode(enum.IntEnum):
BBoxCenter = enum.auto() BBoxCenter = enum.auto()
AxisCenter = enum.auto() AxisCenter = enum.auto()
Max = enum.auto() Max = enum.auto()
_g_AlignModeDesc: dict[AlignMode, tuple[str, str]] = { _g_AlignModeDesc: dict[AlignMode, tuple[str, str, str]] = {
AlignMode.Min: ("Min", "The min value in specified axis."), AlignMode.Min: ("Min", "The min value in specified axis.", "REMOVE"),
AlignMode.BBoxCenter: ("Center (Bounding Box)", "The bounding box center in specified axis."), AlignMode.BBoxCenter: ("Center (Bounding Box)", "The bounding box center in specified axis.", "SHADING_BBOX"),
AlignMode.AxisCenter: ("Center (Axis)", "The object's source point in specified axis."), AlignMode.AxisCenter: ("Center (Axis)", "The object's source point in specified axis.", "OBJECT_ORIGIN"),
AlignMode.Max: ("Max", "The max value in specified axis."), AlignMode.Max: ("Max", "The max value in specified axis.", "ADD"),
} }
_g_EnumHelper_AlignMode = UTIL_functions.EnumPropHelper( _g_EnumHelper_AlignMode = UTIL_functions.EnumPropHelper(
AlignMode, AlignMode,
@ -21,7 +21,23 @@ _g_EnumHelper_AlignMode = UTIL_functions.EnumPropHelper(
lambda x: AlignMode(int(x)), lambda x: AlignMode(int(x)),
lambda x: _g_AlignModeDesc[x][0], lambda x: _g_AlignModeDesc[x][0],
lambda x: _g_AlignModeDesc[x][1], lambda x: _g_AlignModeDesc[x][1],
lambda _: '' lambda x: _g_AlignModeDesc[x][2]
)
class CurrentInstance(enum.IntEnum):
ActiveObject = enum.auto()
Cursor = enum.auto()
_g_CurrentInstanceDesc: dict[CurrentInstance, tuple[str, str, str]] = {
CurrentInstance.ActiveObject: ("Active Object", "Use Active Object as Current Object", "OBJECT_DATA"),
CurrentInstance.Cursor: ("3D Cursor", "Use 3D Cursor as Current Object", "CURSOR"),
}
_g_EnumHelper_CurrentInstance = UTIL_functions.EnumPropHelper(
CurrentInstance,
lambda x: str(x.value),
lambda x: CurrentInstance(int(x)),
lambda x: _g_CurrentInstanceDesc[x][0],
lambda x: _g_CurrentInstanceDesc[x][1],
lambda x: _g_CurrentInstanceDesc[x][2]
) )
#endregion #endregion
@ -55,14 +71,23 @@ class BBP_PG_legacy_align_history(bpy.types.PropertyGroup):
default = False, default = False,
translation_context = 'BBP_PG_legacy_align_history/property' translation_context = 'BBP_PG_legacy_align_history/property'
) # type: ignore ) # type: ignore
current_instance: bpy.props.EnumProperty(
name = "Current Instance",
description = "Decide which instance should be used as Current Object",
items = _g_EnumHelper_CurrentInstance.generate_items(),
default = _g_EnumHelper_CurrentInstance.to_selection(CurrentInstance.ActiveObject),
translation_context = 'BBP_PG_legacy_align_history/property'
) # type: ignore
current_align_mode: bpy.props.EnumProperty( current_align_mode: bpy.props.EnumProperty(
name = "Current Object (Active Object)", name = "Current Object",
description = "The align mode applied to Current Object",
items = _g_EnumHelper_AlignMode.generate_items(), items = _g_EnumHelper_AlignMode.generate_items(),
default = _g_EnumHelper_AlignMode.to_selection(AlignMode.AxisCenter), default = _g_EnumHelper_AlignMode.to_selection(AlignMode.AxisCenter),
translation_context = 'BBP_PG_legacy_align_history/property' translation_context = 'BBP_PG_legacy_align_history/property'
) # type: ignore ) # type: ignore
target_align_mode: bpy.props.EnumProperty( target_align_mode: bpy.props.EnumProperty(
name = "Target Objects (Selected Objects)", name = "Target Objects",
description = "The align mode applied to Target Objects (selected objects except active object if Current Instance is active object)",
items = _g_EnumHelper_AlignMode.generate_items(), items = _g_EnumHelper_AlignMode.generate_items(),
default = _g_EnumHelper_AlignMode.to_selection(AlignMode.AxisCenter), default = _g_EnumHelper_AlignMode.to_selection(AlignMode.AxisCenter),
translation_context = 'BBP_PG_legacy_align_history/property' translation_context = 'BBP_PG_legacy_align_history/property'
@ -148,7 +173,7 @@ class BBP_OT_legacy_align(bpy.types.Operator):
def execute(self, context): def execute(self, context):
# get processed objects # get processed objects
(current_obj, target_objs) = _prepare_objects() (current_obj, current_cursor, target_objs) = _prepare_objects()
# YYC MARK: # YYC MARK:
# This statement is VERY IMPORTANT. # This statement is VERY IMPORTANT.
# If this statement is not presented, Blender will return identity matrix # If this statement is not presented, Blender will return identity matrix
@ -162,7 +187,8 @@ class BBP_OT_legacy_align(bpy.types.Operator):
histories = UTIL_functions.CollectionVisitor(self.align_history) histories = UTIL_functions.CollectionVisitor(self.align_history)
for entry in histories: for entry in histories:
_align_objects( _align_objects(
current_obj, target_objs, _g_EnumHelper_CurrentInstance.get_selection(entry.current_instance),
current_obj, current_cursor, target_objs,
entry.align_x, entry.align_y, entry.align_z, entry.align_x, entry.align_y, entry.align_z,
_g_EnumHelper_AlignMode.get_selection(entry.current_align_mode), _g_EnumHelper_AlignMode.get_selection(entry.current_align_mode),
_g_EnumHelper_AlignMode.get_selection(entry.target_align_mode) _g_EnumHelper_AlignMode.get_selection(entry.target_align_mode)
@ -185,11 +211,22 @@ class BBP_OT_legacy_align(bpy.types.Operator):
row.prop(entry, "align_y", toggle = 1) row.prop(entry, "align_y", toggle = 1)
row.prop(entry, "align_z", toggle = 1) row.prop(entry, "align_z", toggle = 1)
# show mode # show current instance
col.separator() col.separator()
col.label(text='Current Object (Active Object)', text_ctxt='BBP_OT_legacy_align/draw') col.label(text='Current Object', text_ctxt='BBP_OT_legacy_align/draw')
col.prop(entry, "current_align_mode", expand = True) # it should be shown in horizon so we create a new sublayout
col.label(text='Target Objects (Selected Objects)', text_ctxt='BBP_OT_legacy_align/draw') row = col.row()
row.prop(entry, 'current_instance', expand=True)
# show instance and mode
col.separator()
# only show current object mode if current instance is active object,
# because there is no mode for 3d cursor.
current_instnce = _g_EnumHelper_CurrentInstance.get_selection(entry.current_instance)
if current_instnce == CurrentInstance.ActiveObject:
col.label(text='Current Object Align Mode', text_ctxt='BBP_OT_legacy_align/draw')
col.prop(entry, "current_align_mode", expand = True)
col.label(text='Target Objects Align Mode', text_ctxt='BBP_OT_legacy_align/draw')
col.prop(entry, "target_align_mode", expand = True) col.prop(entry, "target_align_mode", expand = True)
# show apply button # show apply button
@ -206,44 +243,66 @@ class BBP_OT_legacy_align(bpy.types.Operator):
#region Core Functions #region Core Functions
def _check_align_requirement() -> bool: def _check_align_requirement() -> bool:
# if we are not in object mode, do not do legacy align # If we are not in object mode, do not do legacy align
if not UTIL_functions.is_in_object_mode(): if not UTIL_functions.is_in_object_mode():
return False return False
# check current obj # YYC MARK:
# We still need to check active object (as current object)
# although we can choose align with active object or 3d cursor.
# Because we can not make any promise that user will
# select Active Object or 3D Cursor as current object before executing this operator.
if bpy.context.active_object is None: if bpy.context.active_object is None:
return False return False
# check target obj with filter of current obj
length = len(bpy.context.selected_objects) # YYC MARK:
if bpy.context.active_object in bpy.context.selected_objects: # Roughly check selected objects.
length -= 1 # We do not need exclude active object from selected objects,
return length != 0 # because active object may be moved when 3D Cursor is current object.
if len(bpy.context.selected_objects) == 0:
return False
return True
def _prepare_objects() -> tuple[bpy.types.Object, set[bpy.types.Object]]: def _prepare_objects() -> tuple[bpy.types.Object, mathutils.Vector, list[bpy.types.Object]]:
# get current object # Fetch current object
current_obj: bpy.types.Object = bpy.context.active_object current_obj = typing.cast(bpy.types.Object, bpy.context.active_object)
# get target objects # Fetch 3d cursor location
target_objs: set[bpy.types.Object] = set(bpy.context.selected_objects) current_cursor: mathutils.Vector = bpy.context.scene.cursor.location
# remove active one
if current_obj in target_objs: # YYC MARK:
target_objs.remove(current_obj) # Fetch target objects and do NOT remove active object from it.
# because active object will be moved when current instance is 3D Cursor.
target_objs: list[bpy.types.Object] = bpy.context.selected_objects[:]
# return value # return value
return (current_obj, target_objs) return (current_obj, current_cursor, target_objs)
def _align_objects( def _align_objects(
current_obj: bpy.types.Object, target_objs: set[bpy.types.Object], current_instance: CurrentInstance,
current_obj: bpy.types.Object, current_cursor: mathutils.Vector, target_objs: list[bpy.types.Object],
align_x: bool, align_y: bool, align_z: bool, current_mode: AlignMode, target_mode: AlignMode) -> None: align_x: bool, align_y: bool, align_z: bool, current_mode: AlignMode, target_mode: AlignMode) -> None:
# if no align, skip # if no align, skip
if not (align_x or align_y or align_z): if not (align_x or align_y or align_z):
return return
# calc current object data # calc current object data
current_obj_ref: mathutils.Vector = _get_object_ref_point(current_obj, current_mode) current_obj_ref: mathutils.Vector
match current_instance:
case CurrentInstance.ActiveObject:
current_obj_ref = _get_object_ref_point(current_obj, current_mode)
case CurrentInstance.Cursor:
current_obj_ref = current_cursor
# process each target obj # process each target obj
for target_obj in target_objs: for target_obj in target_objs:
# YYC MARK:
# If we use active object as current instance, we need exclude it from target objects,
# because there is no pre-exclude considering the scenario that 3D Cursor is current instance.
if current_instance == CurrentInstance.ActiveObject and current_obj == target_obj:
continue
# calc target object data # calc target object data
target_obj_ref: mathutils.Vector = _get_object_ref_point(target_obj, target_mode) target_obj_ref: mathutils.Vector = _get_object_ref_point(target_obj, target_mode)
# build translation transform # build translation transform
@ -256,21 +315,21 @@ def _align_objects(
# apply translation transform to left side (add into original matrix) # apply translation transform to left side (add into original matrix)
target_obj.matrix_world = target_obj_translation_matrix @ target_obj.matrix_world target_obj.matrix_world = target_obj_translation_matrix @ target_obj.matrix_world
bpy.context.scene.update_tag
def _get_object_ref_point(obj: bpy.types.Object, mode: AlignMode) -> mathutils.Vector: def _get_object_ref_point(obj: bpy.types.Object, mode: AlignMode) -> mathutils.Vector:
ref_pos: mathutils.Vector = mathutils.Vector((0, 0, 0)) ref_pos = mathutils.Vector((0, 0, 0))
# calc bounding box data # calc bounding box data
corners: tuple[mathutils.Vector] = tuple(obj.matrix_world @ mathutils.Vector(corner) for corner in obj.bound_box) corners: tuple[mathutils.Vector, ...] = tuple(obj.matrix_world @ mathutils.Vector(corner) for corner in obj.bound_box)
bbox_min_corner: mathutils.Vector = mathutils.Vector((0, 0, 0)) bbox_min_corner = mathutils.Vector((
bbox_min_corner.x = min((vec.x for vec in corners)) min((vec.x for vec in corners)),
bbox_min_corner.y = min((vec.y for vec in corners)) min((vec.y for vec in corners)),
bbox_min_corner.z = min((vec.z for vec in corners)) min((vec.z for vec in corners)),
bbox_max_corner: mathutils.Vector = mathutils.Vector((0, 0, 0)) ))
bbox_max_corner.x = max((vec.x for vec in corners)) bbox_max_corner = mathutils.Vector((
bbox_max_corner.y = max((vec.y for vec in corners)) max((vec.x for vec in corners)),
bbox_max_corner.z = max((vec.z for vec in corners)) max((vec.y for vec in corners)),
max((vec.z for vec in corners)),
))
# return value by given align mode # return value by given align mode
match(mode): match(mode):

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')):
@ -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). # 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)) diff = mathutils.Vector((x2, y2)) - mathutils.Vector((x1, y1))
bld_angle = math.degrees(mathutils.Vector((1,0)).angle_signed(diff, 0)) bld_angle = math.degrees(mathutils.Vector((1,0)).angle_signed(diff, 0))
# flip it first # flip it first
bld_angle = -bld_angle bld_angle = -bld_angle
# process positove number and negative number respectively # 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), '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)), 'scale': lambda x, y, z: mathutils.Matrix.LocRotScale(None, None, (x, y, z)),
'ident': lambda: mathutils.Matrix.Identity(4), 'ident': lambda: mathutils.Matrix.Identity(4),
# my misc custom functions # my misc custom functions
'distance': _env_fct_distance, 'distance': _env_fct_distance,
'angle': _env_fct_angle, 'angle': _env_fct_angle,
@ -191,8 +192,32 @@ 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(),
@ -202,17 +227,20 @@ class EnumPropHelper(UTIL_functions.EnumPropHelper[str]):
lambda _: '', lambda _: '',
lambda x: self.get_bme_showcase_icon(x) lambda x: self.get_bme_showcase_icon(x)
) )
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:
""" """
Get BME display title by prototype identifier. Get BME display title by prototype identifier.
@ -326,14 +354,14 @@ def create_bme_struct(
# create mtl slot remap to help following mesh adding # create mtl slot remap to help following mesh adding
# because mesh writer do not accept string format mtl slot visiting, # because mesh writer do not accept string format mtl slot visiting,
# it only accept int based mtl slot index. # it only accept int based mtl slot index.
# #
# Also we build face used mtl slot index at the same time. # Also we build face used mtl slot index at the same time.
# So we do not analyse texture field again when providing face data. # 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. # 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` # 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. # 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. # 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. # NOTE: since Python 3.6, the item of builtin dict is ordered by inserting order.
# we rely on this to implement following features. # we rely on this to implement following features.
mtl_remap: dict[str, int] = {} mtl_remap: dict[str, int] = {}
@ -351,7 +379,7 @@ def create_bme_struct(
# if existing, no need to add into remap # if existing, no need to add into remap
# but we need get its index from remap # but we need get its index from remap
prebuild_face_mtl_idx[face_idx] = mtl_remap.get(mtl_name, 0) prebuild_face_mtl_idx[face_idx] = mtl_remap.get(mtl_name, 0)
# pre-compute vertices data because we may need used later. # 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 # Because if face normal data is null, it mean that we need to compute it
# by given vertices. # by given vertices.
@ -366,7 +394,7 @@ def create_bme_struct(
cache_bv = typing.cast(mathutils.Vector, transform @ cache_bv) cache_bv = typing.cast(mathutils.Vector, transform @ cache_bv)
# get result # get result
prebuild_vec_data.append((cache_bv.x, cache_bv.y, cache_bv.z)) prebuild_vec_data.append((cache_bv.x, cache_bv.y, cache_bv.z))
# Check whether given transform is mirror matrix # Check whether given transform is mirror matrix
# because mirror matrix will reverse triangle indice order. # because mirror matrix will reverse triangle indice order.
# If matrix is mirror matrix, we need reverse it again in following procession, # 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' 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,80 +9,96 @@ 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 = {
'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 def __extract_prototype(self, prototype: bme.Prototype) -> None:
yield polib.POEntry(msgid=showcase.title, msgstr='', msgctxt=f'{CTX_TRANSLATION}/{identifier}') identifier = prototype.identifier
# Extract showcase entries showcase = prototype.showcase
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}]')
# Show message
logging.info(f'Extracting prototype {identifier}')
def _extract_json(json_file: Path) -> typing.Iterator[polib.POEntry]: # Extract showcase
# Show message if showcase is None:
logging.info(f'Extracting file {json_file}') return
try: # Extract showcase title
# Read file and convert it into BME struct. self.po.append(polib.POEntry(msgid=showcase.title, msgstr='', msgctxt=f'{CTX_PROTOTYPE}/{identifier}'))
with open(json_file, 'r', encoding='utf-8') as f: # extract showcase category
document = json5.load(f) if showcase.category not in self.categories:
prototypes = bme.Prototypes.model_validate(document) self.po.append(polib.POEntry(msgid=showcase.category, msgstr='', msgctxt=CTX_CATEGORY))
# Extract translation self.categories.add(showcase.category)
return itertools.chain.from_iterable(_extract_prototype(prototype) for prototype in prototypes.root) # Extract showcase entries
except pydantic.ValidationError: for i, cfg in enumerate(showcase.cfgs):
logging.error(f'Can not extract translation from {json_file} due to struct error. Please validate it first.') # extract title and description
except (ValueError, UnicodeDecodeError): self.po.append(polib.POEntry(msgid=cfg.title, msgstr='', msgctxt=f'{CTX_PROTOTYPE}/{identifier}/[{i}]'))
logging.error(f'Can not extract translation from {json_file} due to JSON5 error. Please validate it first.') self.po.append(polib.POEntry(msgid=cfg.desc, msgstr='', msgctxt=f'{CTX_PROTOTYPE}/{identifier}/[{i}]'))
# Output nothing def __extract_json(self, json_file: Path) -> None:
return itertools.chain.from_iterable(()) # 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: def extract_jsons(self) -> None:
raw_jsons_dir = common.get_raw_assets_folder(AssetKind.Jsons) raw_jsons_dir = common.get_raw_assets_folder(AssetKind.Jsons)
# Create POT content # Iterate all prototypes and add into POT
po = polib.POFile() for raw_json_file in raw_jsons_dir.glob('*.json5'):
po.metadata = { # Skip non-file.
'Project-Id-Version': '1.0', if not raw_json_file.is_file():
'Report-Msgid-Bugs-To': 'you@example.com', continue
'POT-Creation-Date': 'YEAR-MO-DA HO:MI+ZONE', # Extract json
'PO-Revision-Date': 'YEAR-MO-DA HO:MI+ZONE', self.__extract_json(raw_json_file)
'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 def save(self) -> None:
for raw_json_file in raw_jsons_dir.glob('*.json5'): """Save extracted POT file into correct path"""
# Skip non-file. pot_file = common.get_root_folder() / 'i18n' / 'bme.pot'
if not raw_json_file.is_file(): logging.info(f'Saving POT into {pot_file}')
continue self.po.save(str(pot_file))
# 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))
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)