1 Commits

Author SHA1 Message Date
7f33e4ad92 feat: allow 3D Cursor as align source in legacy align operator.
- allow 3D Cursor as align source in legacy align operator.
- add icon for legacy align.
2025-08-01 14:02:26 +08:00
32 changed files with 583 additions and 1178 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -84,7 +84,6 @@
"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": [
@ -135,7 +134,6 @@
"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,7 +49,6 @@
"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": [
@ -113,7 +112,6 @@
"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": [
@ -177,7 +175,6 @@
"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,12 +1,4 @@
[ [
// 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,
@ -161,11 +153,6 @@
} }
] ]
}, },
// 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,7 +149,6 @@
"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": [
@ -202,7 +201,6 @@
"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": [
@ -255,7 +253,6 @@
"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": [
@ -308,7 +305,6 @@
"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": [
@ -361,7 +357,6 @@
"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": [
@ -414,7 +409,6 @@
"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,7 +228,6 @@
"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": [
@ -279,7 +278,6 @@
"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": [
@ -330,7 +328,6 @@
"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": [
@ -381,7 +378,6 @@
"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": [
@ -432,7 +428,6 @@
"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": [
@ -483,7 +478,6 @@
"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,7 +3,6 @@
"identifier": "floor_flat", "identifier": "floor_flat",
"showcase": { "showcase": {
"title": "Flat", "title": "Flat",
"category": "Miscellaneous",
"icon": "Flat", "icon": "Flat",
"type": "floor", "type": "floor",
"cfgs": [ "cfgs": [
@ -37,7 +36,7 @@
}, },
{ {
"field": "is_sink_", "field": "is_sink_",
"type": "bool", "type": "float",
"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,7 +116,6 @@
"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": [
@ -192,7 +191,6 @@
"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": [
@ -268,7 +266,6 @@
"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,7 +3,6 @@
"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": [
@ -143,7 +142,6 @@
"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,5 +1,4 @@
[ [
// The shared template prototype used by all floor terminals.
{ {
"identifier": "raw_floor_terminal", "identifier": "raw_floor_terminal",
"showcase": null, "showcase": null,
@ -23,30 +22,26 @@
"faces": [], "faces": [],
"instances": [ "instances": [
{ {
"identifier": "cv_trapezoid_side", "identifier": "cv_triangle_side",
"skip": "False", "skip": "False",
"params": { "params": {
"long_edge_length": "5.0", "edge_length": "2.5",
"short_edge_offset": "2.5", "tip_offset": "2.5",
"short_edge_length": "2.5",
"height": "height", "height": "height",
"face": "(face[0], False, False, face[3], face[4], False)", "face": "(face[0], False, False, face[3], face[4], None)",
"is_sink": "is_sink", "is_sink": "is_sink"
"is_ribbon": "False"
}, },
"transform": "ident()" "transform": "ident()"
}, },
{ {
"identifier": "cv_trapezoid_side", "identifier": "cv_triangle_side",
"skip": "False", "skip": "False",
"params": { "params": {
"long_edge_length": "5.0", "edge_length": "2.5",
"short_edge_offset": "2.5", "tip_offset": "2.5",
"short_edge_length": "2.5",
"height": "height", "height": "height",
"face": "(face[0], False, False, face[3], face[5], False)", "face": "(face[0], False, False, face[3], face[5], None)",
"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)"
}, },
@ -66,7 +61,7 @@
"identifier": "floor_rectangle_bottom", "identifier": "floor_rectangle_bottom",
"skip": "not face[1]", "skip": "not face[1]",
"params": { "params": {
"length": "5", "length": "2.5",
"width": "5" "width": "5"
}, },
"transform": "move(0, 0, -height)" "transform": "move(0, 0, -height)"
@ -77,7 +72,6 @@
"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": [
@ -128,7 +122,6 @@
"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,7 +137,6 @@
"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": [
@ -188,7 +187,6 @@
"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": [
@ -239,7 +237,6 @@
"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,7 +111,6 @@
"identifier": "floor_transition", "identifier": "floor_transition",
"showcase": { "showcase": {
"title": "Transition", "title": "Transition",
"category": "Miscellaneous",
"icon": "Transition", "icon": "Transition",
"type": "floor", "type": "floor",
"cfgs": [ "cfgs": [
@ -192,7 +191,6 @@
"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,7 +3,6 @@
"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": [
@ -107,7 +106,6 @@
"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": [
@ -168,50 +166,34 @@
"transform": "rot(0, 0, 90) @ scale(1, -1, 1)" "transform": "rot(0, 0, 90) @ scale(1, -1, 1)"
}, },
{ {
"identifier": "cv_trapezoid_side", "identifier": "cv_triangle_side",
"skip": "False", "skip": "False",
"params": { "params": {
"long_edge_length": "5.0", "edge_length": "2.5",
"short_edge_offset": "2.5", "tip_offset": "2.5",
"short_edge_length": "2.5",
"height": "height", "height": "height",
"face": "(face[0], False, False, face[3], face[4], False)", "face": "(face[0], False, False, face[3], face[4], None)",
"is_sink": "True", "is_sink": "True"
"is_ribbon": "False"
}, },
"transform": "ident()" "transform": "ident()"
}, },
{ {
"identifier": "cv_trapezoid_side", "identifier": "cv_triangle_side",
"skip": "False", "skip": "False",
"params": { "params": {
"long_edge_length": "5.0", "edge_length": "2.5",
"short_edge_offset": "2.5", "tip_offset": "2.5",
"short_edge_length": "2.5",
"height": "height", "height": "height",
"face": "(face[0], False, False, face[3], face[5], False)", "face": "(face[0], False, False, face[3], face[5], None)",
"is_sink": "True",
"is_ribbon": "False"
},
"transform": "move(0, width + 5, 0) @ scale(1, -1, 1)"
},
{
"identifier": "floor_flat",
"skip": "False",
"params": {
"height": "height",
"length": "2.5",
"width": "width",
"face": "(face[0], False, False, face[3], False, False)",
"is_sink": "True" "is_sink": "True"
}, },
"transform": "move(2.5, 2.5, 0)" "transform": "move(0, width + 5, 0) @ scale(1, -1, 1)"
}, },
{ {
"identifier": "floor_rectangle_bottom", "identifier": "floor_rectangle_bottom",
"skip": "not face[1]", "skip": "not face[1]",
"params": { "params": {
"length": "5", "length": "2.5",
"width": "5 + width" "width": "5 + width"
}, },
"transform": "move(0, 0, -height)" "transform": "move(0, 0, -height)"
@ -222,7 +204,6 @@
"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": [
@ -355,7 +336,6 @@
"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": [
@ -479,7 +459,6 @@
"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', toggle=1, text='Yes', text_ctxt='BBP_OT_add_bme_struct/draw') box_layout.prop(op_cfgs_visitor[cfg_index], 'prop_bool', text='')
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,25 +280,17 @@ 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 category, idents in _g_EnumHelper_BmeStructType.get_bme_categories().items(): for ident in _g_EnumHelper_BmeStructType.get_bme_identifiers():
# 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_title_context(ident), text_ctxt = UTIL_translation.build_prototype_showcase_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, PROP_preferences from . import UTIL_rail_creator
## Const Value Hint: ## Const Value Hint:
# Default Rail Radius: 0.35 (in measure) # Default Rail Radius: 0.35 (in measure)
@ -233,10 +233,6 @@ 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(
@ -258,10 +254,6 @@ 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),
@ -280,10 +272,6 @@ 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(
@ -313,10 +301,6 @@ 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(
@ -356,10 +340,6 @@ 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(
@ -399,10 +379,6 @@ 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(
@ -454,10 +430,6 @@ 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(
@ -502,10 +474,6 @@ 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,15 +19,9 @@ 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'}
@ -44,16 +38,10 @@ 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(
filename, self.general_get_filename(),
encodings, encodings,
texture_save_opt, texture_save_opt,
self.general_get_use_compress(), self.general_get_use_compress(),
@ -80,7 +68,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,12 +19,6 @@ 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)
@ -32,14 +26,8 @@ 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(
filename, self.general_get_filename(),
encodings, encodings,
self.general_get_conflict_resolver() self.general_get_conflict_resolver()
) )
@ -52,7 +40,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

@ -1,399 +0,0 @@
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 Object', text_ctxt='BBP_OT_legacy_align/draw') col.label(text='Current Instance', 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 Align Mode', text_ctxt='BBP_OT_legacy_align/draw') col.label(text='Current Object (Active Object)', 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 Align Mode', text_ctxt='BBP_OT_legacy_align/draw') col.label(text='Target Objects (Selected Objects)', 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,16 +195,6 @@ 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()
@ -228,11 +218,24 @@ 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)
@ -250,6 +253,9 @@ 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,7 +24,6 @@ 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'
@ -65,10 +64,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 list storing BME prototype.""" ## The dict. Key is prototype identifier. value is the index of prototype in prototype list.
_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')):
@ -193,31 +192,7 @@ 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(),
@ -231,15 +206,12 @@ 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.
In other words, template prototype is not included. Template prototype is not included.
""" """
return self.showcase_identifiers return tuple(
x[TOKEN_IDENTIFIER] # get identifier
def get_bme_categories(self) -> dict[str, tuple[str, ...]]: for x in filter(lambda x: x[TOKEN_SHOWCASE] is not None, _g_BMEPrototypes) # filter() to filter no showcase template.
""" )
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,14 +340,6 @@ 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
@ -372,6 +364,7 @@ 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,22 +55,14 @@ 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 = f'{CTX_BBP}/BME' CTX_BBP_BME: str = CTX_BBP + '/BME'
CTX_BBP_BME_CATEGORY: str = f'{CTX_BBP_BME}/Category' def build_prototype_showcase_context(identifier: str) -> str:
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 f'{CTX_BBP_BME_PROTOTYPE}/{identifier}' return CTX_BBP_BME + '/' + 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.
@ -78,7 +70,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 f'{CTX_BBP_BME_PROTOTYPE}/{identifier}/[{cfg_index}]' return CTX_BBP_BME + f'/{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, OP_OBJECT_game_view from . import OP_OBJECT_legacy_align, OP_OBJECT_virtools_group, OP_OBJECT_snoop_group_then_to_mesh, OP_OBJECT_naming_convention
#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 = typing.cast(bpy.types.UILayout, self.layout) layout = 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,10 +178,6 @@ 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()
@ -350,7 +346,6 @@ 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:
@ -373,7 +368,6 @@ 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", "macos-arm64"] platforms = ["windows-x64", "linux-x64"]
# 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,7 +27,6 @@ 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,36 +9,9 @@ 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'
class JsonsExtractor: def _extract_prototype(prototype: bme.Prototype) -> typing.Iterator[polib.POEntry]:
po: polib.POFile
"""Extracted PO file"""
categories: set[str]
"""Set for removing duplicated category names"""
def __init__(self) -> None:
# create po file
self.po = polib.POFile()
self.po.metadata = {
'Project-Id-Version': '1.0',
'Report-Msgid-Bugs-To': 'you@example.com',
'POT-Creation-Date': 'YEAR-MO-DA HO:MI+ZONE',
'PO-Revision-Date': 'YEAR-MO-DA HO:MI+ZONE',
'Last-Translator': 'FULL NAME <EMAIL@ADDRESS>',
'Language-Team': 'LANGUAGE <LL@li.org>',
'MIME-Version': '1.0',
'Content-Type': 'text/plain; charset=utf-8',
'Content-Transfer-Encoding': '8bit',
'X-Generator': 'polib',
}
# create category set
self.categories = set()
def __extract_prototype(self, prototype: bme.Prototype) -> None:
identifier = prototype.identifier identifier = prototype.identifier
showcase = prototype.showcase showcase = prototype.showcase
@ -50,18 +23,15 @@ class JsonsExtractor:
return return
# Extract showcase title # Extract showcase title
self.po.append(polib.POEntry(msgid=showcase.title, msgstr='', msgctxt=f'{CTX_PROTOTYPE}/{identifier}')) yield polib.POEntry(msgid=showcase.title, msgstr='', msgctxt=f'{CTX_TRANSLATION}/{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 # Extract showcase entries
for i, cfg in enumerate(showcase.cfgs): for i, cfg in enumerate(showcase.cfgs):
# extract title and description # extract title and description
self.po.append(polib.POEntry(msgid=cfg.title, msgstr='', msgctxt=f'{CTX_PROTOTYPE}/{identifier}/[{i}]')) yield polib.POEntry(msgid=cfg.title, msgstr='', msgctxt=f'{CTX_TRANSLATION}/{identifier}/[{i}]')
self.po.append(polib.POEntry(msgid=cfg.desc, msgstr='', msgctxt=f'{CTX_PROTOTYPE}/{identifier}/[{i}]')) yield polib.POEntry(msgid=cfg.desc, msgstr='', msgctxt=f'{CTX_TRANSLATION}/{identifier}/[{i}]')
def __extract_json(self, json_file: Path) -> None:
def _extract_json(json_file: Path) -> typing.Iterator[polib.POEntry]:
# Show message # Show message
logging.info(f'Extracting file {json_file}') logging.info(f'Extracting file {json_file}')
@ -71,34 +41,48 @@ class JsonsExtractor:
document = json5.load(f) document = json5.load(f)
prototypes = bme.Prototypes.model_validate(document) prototypes = bme.Prototypes.model_validate(document)
# Extract translation # Extract translation
for prototype in prototypes.root: return itertools.chain.from_iterable(_extract_prototype(prototype) for prototype in prototypes.root)
self.__extract_prototype(prototype)
except pydantic.ValidationError: except pydantic.ValidationError:
logging.error( logging.error(f'Can not extract translation from {json_file} due to struct error. Please validate it first.')
f'Can not extract translation from {json_file} due to struct error. Please validate it first.')
except (ValueError, UnicodeDecodeError): except (ValueError, UnicodeDecodeError):
logging.error(f'Can not extract translation from {json_file} due to JSON5 error. Please validate it first.') logging.error(f'Can not extract translation from {json_file} due to JSON5 error. Please validate it first.')
def extract_jsons(self) -> None: # Output nothing
return itertools.chain.from_iterable(())
def extract_jsons() -> None:
raw_jsons_dir = common.get_raw_assets_folder(AssetKind.Jsons) raw_jsons_dir = common.get_raw_assets_folder(AssetKind.Jsons)
# Create POT content
po = polib.POFile()
po.metadata = {
'Project-Id-Version': '1.0',
'Report-Msgid-Bugs-To': 'you@example.com',
'POT-Creation-Date': 'YEAR-MO-DA HO:MI+ZONE',
'PO-Revision-Date': 'YEAR-MO-DA HO:MI+ZONE',
'Last-Translator': 'FULL NAME <EMAIL@ADDRESS>',
'Language-Team': 'LANGUAGE <LL@li.org>',
'MIME-Version': '1.0',
'Content-Type': 'text/plain; charset=utf-8',
'Content-Transfer-Encoding': '8bit',
'X-Generator': 'polib',
}
# Iterate all prototypes and add into POT # 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 # Extract json and append it.
self.__extract_json(raw_json_file) po.extend(_extract_json(raw_json_file))
def save(self) -> None: # Write into POT file
"""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}')
self.po.save(str(pot_file)) po.save(str(pot_file))
if __name__ == '__main__': if __name__ == '__main__':
common.setup_logging() common.setup_logging()
extractor = JsonsExtractor() extract_jsons()
extractor.extract_jsons()
extractor.save()

View File

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