23 Commits

Author SHA1 Message Date
e22b888bfc doc: fix bme adder doc 2025-09-01 13:16:02 +08:00
88ef1d3202 doc: fix legacy align doc 2025-09-01 13:05:51 +08:00
f2af90c876 doc: fix export target for virtools exporting in doc 2025-09-01 12:56:19 +08:00
4dba3c3a71 doc: add game camera doc 2025-09-01 11:00:34 +08:00
e31a677d83 doc: add version rules in doc 2025-09-01 10:12:44 +08:00
35fcbe54b5 fix: re-design the layout of game camera.
- use more friendly layout in game camera. reported by zzq.
2025-08-30 22:50:43 +08:00
9e83fe0a10 i18n: update i18n
- update i18n template and translation.
- fix lost translation context in code.
2025-08-26 21:54:32 +08:00
33fb1a65d3 chore: remove useless gitkeep 2025-08-26 20:47:06 +08:00
415cc98758 doc: update document
- add hint for gray virtools import export button
- add outcome of leaving blank ballance texture folder setting.
- update compile chapter according to the change of build scripts.
2025-08-26 20:42:11 +08:00
2d93ce1340 feat: allow export all object for virtools file. 2025-08-26 19:59:36 +08:00
1129872234 fix: fix UTIL_naming_convention rename in i18n files. 2025-08-25 14:22:42 +08:00
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
42 changed files with 3274 additions and 2218 deletions

2
.gitattributes vendored
View File

@ -3,4 +3,4 @@
# Element placeholder mesh should be save as binary
*.ph binary
# Raw json data should be binary, although i edit it manually
assets/jsons/*.json binary
assets/jsons/*.json5 binary

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -77,6 +77,7 @@
"identifier": "floor_normal_terminal",
"showcase": {
"title": "Normal Floor Terminal",
"category": "Floors",
"icon": "NormalFloorTerminal",
"type": "floor",
"cfgs": [
@ -127,6 +128,7 @@
"identifier": "floor_sink_terminal",
"showcase": {
"title": "Sink Floor Terminal",
"category": "Floors",
"icon": "SinkFloorTerminal",
"type": "floor",
"cfgs": [

View File

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

View File

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

View File

@ -3,6 +3,7 @@
"identifier": "floor_wide_straight",
"showcase": {
"title": "Wide Floor",
"category": "Wide Floors",
"icon": "WideFloor",
"type": "floor",
"cfgs": [
@ -106,6 +107,7 @@
"identifier": "floor_wide_terminal",
"showcase": {
"title": "Wide Floor Terminal",
"category": "Wide Floors",
"icon": "WideFloorTerminal",
"type": "floor",
"cfgs": [
@ -220,6 +222,7 @@
"identifier": "floor_wide_l_crossing",
"showcase": {
"title": "Wide Floor L Crossing",
"category": "Wide Floors",
"icon": "WideLCrossing",
"type": "floor",
"cfgs": [
@ -352,6 +355,7 @@
"identifier": "floor_wide_t_crossing",
"showcase": {
"title": "Wide Floor T Crossing",
"category": "Wide Floors",
"icon": "WideTCrossing",
"type": "floor",
"cfgs": [
@ -475,6 +479,7 @@
"identifier": "floor_wide_x_crossing",
"showcase": {
"title": "Wide Floor X Crossing",
"category": "Wide Floors",
"icon": "WideXCrossing",
"type": "floor",
"cfgs": [

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,471 @@
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),
translation_context = 'BBP_OT_game_resolution/property'
) # 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),
translation_context = 'BBP_OT_game_camera/property'
) # 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),
translation_context = 'BBP_OT_game_camera/property'
) # type: ignore
preset_rotation_angle: bpy.props.EnumProperty(
# I18N: Property not showen should not have name and desc.
# name = "Preset Rotation Angle",
# description = "",
options = {'HIDDEN'},
items = _g_EnumHelper_RotationAngle.generate_items(),
default = _g_EnumHelper_RotationAngle.to_selection(RotationAngle.Deg0),
) # type: ignore
def preset_rotation_angle_deg_getter(self, probe) -> bool:
return _g_EnumHelper_RotationAngle.get_selection(self.preset_rotation_angle) == probe
def preset_rotation_angle_deg_setter(self, val) -> None:
self.preset_rotation_angle = _g_EnumHelper_RotationAngle.to_selection(val)
return None
preset_rotation_angle_deg0: bpy.props.BoolProperty(
name = "0 Degree",
translation_context = 'BBP_OT_game_camera/property',
get = lambda self: BBP_OT_game_camera.preset_rotation_angle_deg_getter(self, RotationAngle.Deg0),
set = lambda self, _: BBP_OT_game_camera.preset_rotation_angle_deg_setter(self, RotationAngle.Deg0)
) # type: ignore
preset_rotation_angle_deg45: bpy.props.BoolProperty(
name = "45 Degree",
translation_context = 'BBP_OT_game_camera/property',
get = lambda self: BBP_OT_game_camera.preset_rotation_angle_deg_getter(self, RotationAngle.Deg45),
set = lambda self, _: BBP_OT_game_camera.preset_rotation_angle_deg_setter(self, RotationAngle.Deg45)
) # type: ignore
preset_rotation_angle_deg90: bpy.props.BoolProperty(
name = "90 Degree",
translation_context = 'BBP_OT_game_camera/property',
get = lambda self: BBP_OT_game_camera.preset_rotation_angle_deg_getter(self, RotationAngle.Deg90),
set = lambda self, _: BBP_OT_game_camera.preset_rotation_angle_deg_setter(self, RotationAngle.Deg90)
) # type: ignore
preset_rotation_angle_deg135: bpy.props.BoolProperty(
name = "135 Degree",
translation_context = 'BBP_OT_game_camera/property',
get = lambda self: BBP_OT_game_camera.preset_rotation_angle_deg_getter(self, RotationAngle.Deg135),
set = lambda self, _: BBP_OT_game_camera.preset_rotation_angle_deg_setter(self, RotationAngle.Deg135)
) # type: ignore
preset_rotation_angle_deg180: bpy.props.BoolProperty(
name = "180 Degree",
translation_context = 'BBP_OT_game_camera/property',
get = lambda self: BBP_OT_game_camera.preset_rotation_angle_deg_getter(self, RotationAngle.Deg180),
set = lambda self, _: BBP_OT_game_camera.preset_rotation_angle_deg_setter(self, RotationAngle.Deg180)
) # type: ignore
preset_rotation_angle_deg225: bpy.props.BoolProperty(
name = "225 Degree",
translation_context = 'BBP_OT_game_camera/property',
get = lambda self: BBP_OT_game_camera.preset_rotation_angle_deg_getter(self, RotationAngle.Deg225),
set = lambda self, _: BBP_OT_game_camera.preset_rotation_angle_deg_setter(self, RotationAngle.Deg225)
) # type: ignore
preset_rotation_angle_deg270: bpy.props.BoolProperty(
name = "270 Degree",
translation_context = 'BBP_OT_game_camera/property',
get = lambda self: BBP_OT_game_camera.preset_rotation_angle_deg_getter(self, RotationAngle.Deg270),
set = lambda self, _: BBP_OT_game_camera.preset_rotation_angle_deg_setter(self, RotationAngle.Deg270)
) # type: ignore
preset_rotation_angle_deg315: bpy.props.BoolProperty(
name = "315 Degree",
translation_context = 'BBP_OT_game_camera/property',
get = lambda self: BBP_OT_game_camera.preset_rotation_angle_deg_getter(self, RotationAngle.Deg315),
set = lambda self, _: BBP_OT_game_camera.preset_rotation_angle_deg_setter(self, RotationAngle.Deg315)
) # type: ignore
custom_rotation_angle: bpy.props.FloatProperty(
name = "Custom Rotation Angle",
description = "The rotation angle of camera relative to 3D Cursor or Active Object",
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,
translation_context = 'BBP_OT_game_camera/property'
) # 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),
translation_context = 'BBP_OT_game_camera/property'
) # 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:
# for preset angles, we show a special layout (grid view)
subgrid = layout.grid_flow(row_major=True, columns=3, even_columns=True, even_rows=True, align=True)
subgrid.prop(self, 'preset_rotation_angle_deg315', toggle = 1)
subgrid.prop(self, 'preset_rotation_angle_deg0', toggle = 1)
subgrid.prop(self, 'preset_rotation_angle_deg45', toggle = 1)
subgrid.prop(self, 'preset_rotation_angle_deg270', toggle = 1)
subicon = subgrid.row()
subicon.alignment = 'CENTER'
subicon.label(text='', icon='MESH_CIRCLE') # show a 3d circle as icon
subgrid.prop(self, 'preset_rotation_angle_deg90', toggle = 1)
subgrid.prop(self, 'preset_rotation_angle_deg225', toggle = 1)
subgrid.prop(self, 'preset_rotation_angle_deg180', toggle = 1)
subgrid.prop(self, 'preset_rotation_angle_deg135', toggle = 1)
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

@ -137,7 +137,7 @@ class BBP_OT_legacy_align(bpy.types.Operator):
return None
apply_flag: bpy.props.BoolProperty(
# TR: Property not showen should not have name and desc.
# I18N: Property not showen should not have name and desc.
# name = "Apply Flag",
# description = "Internal flag.",
options = {'HIDDEN', 'SKIP_SAVE'},
@ -145,14 +145,14 @@ class BBP_OT_legacy_align(bpy.types.Operator):
update = apply_flag_updated
) # type: ignore
recursive_hinder: bpy.props.BoolProperty(
# TR: Property not showen should not have name and desc.
# I18N: Property not showen should not have name and desc.
# name = "Recursive Hinder",
# description = "An internal flag to prevent the loop calling to apply_flags's updator.",
options = {'HIDDEN', 'SKIP_SAVE'},
default = False
) # type: ignore
align_history : bpy.props.CollectionProperty(
# TR: Property not showen should not have name and desc.
# I18N: Property not showen should not have name and desc.
# name = "Historys",
# description = "Align history.",
type = BBP_PG_legacy_align_history
@ -213,7 +213,7 @@ class BBP_OT_legacy_align(bpy.types.Operator):
# show current instance
col.separator()
col.label(text='Current Instance', text_ctxt='BBP_OT_legacy_align/draw')
col.label(text='Current Object', text_ctxt='BBP_OT_legacy_align/draw')
# it should be shown in horizon so we create a new sublayout
row = col.row()
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.
current_instnce = _g_EnumHelper_CurrentInstance.get_selection(entry.current_instance)
if current_instnce == CurrentInstance.ActiveObject:
col.label(text='Current Object (Active Object)', text_ctxt='BBP_OT_legacy_align/draw')
col.label(text='Current Object Align Mode', text_ctxt='BBP_OT_legacy_align/draw')
col.prop(entry, "current_align_mode", expand = True)
col.label(text='Target Objects (Selected Objects)', text_ctxt='BBP_OT_legacy_align/draw')
col.label(text='Target Objects Align Mode', text_ctxt='BBP_OT_legacy_align/draw')
col.prop(entry, "target_align_mode", expand = True)
# show apply button

View File

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

View File

@ -954,6 +954,7 @@ class BBP_OT_preset_virtools_material(bpy.types.Operator):
name = "Preset",
description = "The preset which you want to apply.",
items = _g_Helper_MtlPreset.generate_items(),
translation_context = 'BBP_OT_preset_virtools_material/property'
) # type: ignore
@classmethod

View File

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

View File

@ -245,10 +245,12 @@ class ExportMode(enum.IntEnum):
BldColl = enum.auto()
BldObj = enum.auto()
BldSelObjs = enum.auto()
BldAllObjs = enum.auto()
_g_ExportModeDesc: dict[ExportMode, tuple[str, str, str]] = {
ExportMode.BldColl: ('Collection', 'Export a collection', 'OUTLINER_COLLECTION'),
ExportMode.BldObj: ('Object', 'Export an object', 'OBJECT_DATA'),
ExportMode.BldSelObjs: ('Selected Objects', 'Export selected objects', 'SELECT_SET'),
ExportMode.BldAllObjs: ('All Objects', 'Export all objects stored in this file', 'FILE_BLEND'),
}
_g_EnumHelper_ExportMode = UTIL_functions.EnumPropHelper(
ExportMode,
@ -275,10 +277,8 @@ class ExportParams():
header.label(text='Export Parameters', text_ctxt='BBP/UTIL_ioport_shared.ExportParams/draw')
if body is None: return
# make prop expand horizontaly, not vertical.
horizon_body = body.row()
# draw switch
horizon_body.prop(self, "export_mode", expand=True)
body.prop(self, "export_mode", expand=True)
# draw picker
export_mode = _g_EnumHelper_ExportMode.get_selection(self.export_mode)
@ -290,6 +290,8 @@ class ExportParams():
ptrprops.draw_export_object(body)
case ExportMode.BldSelObjs:
pass # Draw nothing
case ExportMode.BldAllObjs:
pass # Draw nothing
def general_get_export_objects(self, context: bpy.types.Context) -> tuple[bpy.types.Object, ...] | None:
"""
@ -308,6 +310,8 @@ class ExportParams():
else: return (obj, )
case ExportMode.BldSelObjs:
return tuple(context.selected_objects)
case ExportMode.BldAllObjs:
return tuple(bpy.data.objects)
#endregion
@ -340,6 +344,14 @@ class VirtoolsParams():
translation_context = 'BBP/UTIL_ioport_shared.VirtoolsParams/property'
) # type: ignore
def preset_vt_encodings_if_possible(self, context: bpy.types.Context):
"""
Set preset value for Virtools Encoding list if there is no value inside it.
"""
ptrprops = PROP_ptrprop_resolver.PropsVisitor(context.scene)
if len(ptrprops.get_ioport_encodings()) == 0:
ptrprops.preset_ioport_encodings()
def draw_virtools_params(self, context: bpy.types.Context, layout: bpy.types.UILayout, is_importer: bool) -> None:
header: bpy.types.UILayout
body: bpy.types.UILayout
@ -364,7 +376,6 @@ class VirtoolsParams():
if self.use_compress:
body.prop(self, 'compress_level')
def general_get_vt_encodings(self, context: bpy.types.Context) -> tuple[str, ...]:
# get from ptrprop resolver then filter empty item
ptrprops = PROP_ptrprop_resolver.PropsVisitor(context.scene)

View File

@ -48,21 +48,29 @@ import bpy
# - If we use `bpy.app.translations.pgettext` with other non-Blender functions, such as `print`.
# * Use it as a normal function.
#
# All translation annotation are started with `TR:`
# All translation annotation are started with `I18N:`
#
# The universal translation context prefix for BBP_NG plugin.
CTX_BBP: str = 'BBP'
# The universal translation context prefix for BME module in BBP_NG plugin.
CTX_BBP_BME: str = CTX_BBP + '/BME'
def build_prototype_showcase_context(identifier: str) -> str:
CTX_BBP_BME: str = f'{CTX_BBP}/BME'
CTX_BBP_BME_CATEGORY: str = f'{CTX_BBP_BME}/Category'
CTX_BBP_BME_PROTOTYPE: str = f'{CTX_BBP_BME}/Proto'
def build_prototype_showcase_category_context() -> str:
"""
Build the context for getting the translation for BME prototype showcase category.
@return The context for getting translation.
"""
return CTX_BBP_BME_CATEGORY
def build_prototype_showcase_title_context(identifier: str) -> str:
"""
Build the context for getting the translation for BME prototype showcase title.
@param[in] identifier The identifier of this prototype.
@return The context for getting translation.
"""
return CTX_BBP_BME + '/' + identifier
return f'{CTX_BBP_BME_PROTOTYPE}/{identifier}'
def build_prototype_showcase_cfg_context(identifier: str, cfg_index: int) -> str:
"""
Build the context for getting the translation for BME prototype showcase configuration title or description.
@ -70,7 +78,7 @@ def build_prototype_showcase_cfg_context(identifier: str, cfg_index: int) -> str
@param[in] cfg_index The index of this configuration in this prototype showcase.
@return The context for getting translation.
"""
return CTX_BBP_BME + f'/{identifier}/[{cfg_index}]'
return f'{CTX_BBP_BME_PROTOTYPE}/{identifier}/[{cfg_index}]'
#endregion

View File

@ -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_MTL_fix_materials
from . import OP_ADDS_component, OP_ADDS_bme, OP_ADDS_rail
from . import OP_OBJECT_legacy_align, OP_OBJECT_virtools_group, OP_OBJECT_snoop_group_then_to_mesh, OP_OBJECT_naming_convention
from . import OP_OBJECT_legacy_align, OP_OBJECT_virtools_group, OP_OBJECT_snoop_group_then_to_mesh, OP_OBJECT_naming_convention, OP_OBJECT_game_view
#endregion
@ -170,7 +170,7 @@ class BBP_MT_View3DMenu(bpy.types.Menu):
bl_translation_context = 'BBP_MT_View3DMenu'
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.operator(OP_UV_flatten_uv.BBP_OT_flatten_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.operator(OP_OBJECT_legacy_align.BBP_OT_legacy_align.bl_idname)
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.operator(OP_OBJECT_virtools_group.BBP_OT_select_object_by_virtools_group.bl_idname)
layout.separator()
@ -346,6 +350,7 @@ def register() -> None:
OP_OBJECT_virtools_group.register()
OP_OBJECT_snoop_group_then_to_mesh.register()
OP_OBJECT_naming_convention.register()
OP_OBJECT_game_view.register()
# register other classes
for cls in g_BldClasses:
@ -368,6 +373,7 @@ def unregister() -> None:
bpy.utils.unregister_class(cls)
# unregister modules
OP_OBJECT_game_view.unregister()
OP_OBJECT_naming_convention.unregister()
OP_OBJECT_snoop_group_then_to_mesh.unregister()
OP_OBJECT_virtools_group.unregister()

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 80 KiB

View File

@ -1,15 +1,25 @@
# 添加路面
!!! info "BME是可扩展的"
BME的路面添加器是可扩展的菜单中的每一个项实际上都由一组JSON数据描述。您可以阅读[技术信息](./tech-infos.md)章节来了解我们是如何编写这些JSON的甚至您还可以根据你的需求自行扩展BME可创建的路面种类。
## 开始生成
### 从添加菜单生成
在3D视图中点击`Add - Floors`可展开添加路面菜单。菜单如下图所示。
![](../imgs/bme-adder.png)
点击菜单后可以在弹出的子菜单中查看所有受支持的路面类型。其名称和图标提示了它所要创建路面的样式与形状。
点击菜单后可以在弹出的子菜单中按分类查看所有受支持的路面类型。其名称和图标提示了它所要创建路面的样式与形状。
!!! info "BME是可扩展的"
BME的路面添加器是可扩展的菜单中的每一个项实际上都由一组JSON数据描述。您可以阅读[技术信息](./tech-infos.md)章节来了解我们是如何编写这些JSON的甚至您还可以根据你的需求自行扩展BME可创建的路面种类。
### 从侧边栏生成
此外还可以通过点按N键打开3D视图的侧边栏在其中找到Ballance选项卡展开Floor面板也可找到如下图所示
![](../imgs/bme-adder-sidebar.png)
该面板相较于添加菜单,其好处在于可常驻在界面之中,避免了在持续添加路面的操作中,频繁打开菜单寻找路面的麻烦。此外,该选项卡中还有用于添加钢轨和机关的面板可供展开,在后续章节中不再赘述。
## 配置路面

View File

@ -12,11 +12,29 @@ BBP的Virtools文件原生导入导出功能依赖BMap以及其Python绑定PyBMa
然后我们需要将配置好的PyBMap拷贝到本项目的根目录下即可完成此步即存在`bbp_ng/PyBMap`文件夹为配置好的PyBMap
## 生成缩略图和压缩JSON
## 生成资源
BBP内置了一系列自定义图标以及其组件BME需要的用于描述结构的JSON文件。通过批量生成缩略图和压缩JSON的操作可以减小这些部分的大小使得其适合在Blender中加载也更方便分发
BBP的正常运行离不开一系列资源文件,而这些资源文件则需要一些处理才能够正常使用
转到`bbp_ng/tools`文件夹下,`python3 build_icons.py`将批量生成缩略图此功能需要PIL库请提前通过pip安装。其实际上是将`bbp_ng/raw_icons`目录下的原始图片生成对应的缩略图并存储于`bbp_ng/icons`文件夹下。运行`python3 build_jsons.py`将压缩JSON。其实际上是将`bbp_ng/raw_jsons`目录下的原始JSON文件读取压缩再写入到`bbp_ng/jsons`文件夹下
为了生成这些资源文件,首先需要转到`scripts`文件夹下,`uv sync`指令来还原脚本环境需提前安装Astral UV
### 生成缩略图
BBP内置了一系列自定义图标但这些图标都以其原始大小存储在库中。通过批量生成缩略图的操作可以减小这些部分的大小使得其适合在Blender中加载也更方便分发。
执行`uv run build_icons.py`来生成缩略图。其实际上是将`assets/icons`目录下的原始图片生成对应的缩略图并存储于`bbp_ng/icons`文件夹下。
### 生成JSON文件
BBP中的BME组件依赖一系列JSON文件来描述原型。这些描述文件以JSON5格式存储在库中方便编写者阅读。通过批量生成操作将这些JSON5文件转换为JSON文件并压缩其大小可以方便其在Blender中加载以及方便插件分发。
如果你是插件开发者或者是这些原型的编写者那么你在生成JSON文件前还需要额外地进行一项操作验证JSON文件的正确性。BBP插件在加载这些JSON文件时会默认这些文件都是正确的无错误的。如果将有错误的JSON文件放入例如缺少部分字段或者拼写错误等则会导致Blender在创建原型时抛出错误。所以验证JSON文件的正确性很有必要。执行`uv run validate_jsons.py`来验证所有原型文件。如果没有任何报错则验证无误。需要注意的是验证器并非完美的它只能尽可能地验证数据确保一些常见的错误例如字段名称拼写错误等不会发生并不能100%保证验证后的文件没有错误。
对于编译人员而言,只需要执行`uv run build_jsons.py`来生成JSON文件即可。其实际上是将`assets/jsons`目录下的原始JSON5文件读取压缩再以JSON格式写入到`bbp_ng/jsons`文件夹下。
### 生成机关网格
BBP中内置了Ballance所有机关占位符的网格信息。执行`uv run build_meshes.py`来部署这些内容,其简单地将`assets/meshes`下的网格文件复制到`bbp_ng/meshes`文件夹下。
## 翻译
@ -31,7 +49,7 @@ Blender对于插件的多语言支持不尽如人意且BBP的设计比较特
### 提取翻译模板
在翻译之前首先你需要意识到BBP需要翻译的文本由两部分组成一部分是BBP插件本身可以通过Blender自带的多语言插件来实现待翻译文本的提取。另一部分是BME组件中的用于描述结构的JSON文件其中各个展示用字段的名称需要进行翻译而这一部分Blender的多语言插件无能为力因为它是动态加载的。幸运的是我们已经写好了一个提取器可以提取BME的JSON文件中的相关待翻译文本当你在进行上一步压缩JSON的操作时实际上压缩器也一并运行了提取待翻译文本写入`bbp_ng/i18n/bme.pot`文件中。那么接下来的任务就只剩下提取插件部分的翻译了。
在翻译之前首先你需要意识到BBP需要翻译的文本由两部分组成一部分是BBP插件本身可以通过Blender自带的多语言插件来实现待翻译文本的提取。另一部分是BME组件中的用于描述结构的JSON文件其中各个展示用字段的名称需要进行翻译而这一部分Blender的多语言插件无能为力因为它是动态加载的。幸运的是我们已经写好了一个提取器可以提取BME的JSON文件中的相关待翻译文本。就是在上一步运行脚本的文件夹中,执行`uv run extract_jsons.py`,脚本就会提取待翻译文本写入`i18n/bme.pot`文件中。那么接下来的任务就只剩下提取插件部分的翻译了。
首先你需要启用Blender内置的多语言插件Manage UI translations。为了启用它你可能还需要下载对应Blender版本的源代码和翻译仓库具体操作方法可参考[Blender的官方文档](https://developer.blender.org/docs/handbook/translating/translator_guide/)。在启用插件并在偏好设置中配置了合适的相关路径后你就可以在Render面板下找到I18n Update Translation面板接下来就可以提取翻译了。按照以下步骤提取翻译
@ -41,11 +59,11 @@ Blender对于插件的多语言支持不尽如人意且BBP的设计比较特
* Simplified Chinese (简体中文)
1. 点击最下方一栏的Refresh I18n Data按钮然后在弹出的窗口中选择Ballance Blender Plugin等待一会后插件就会完成待翻译字符的提取。此时插件只是将他们按照Blender推荐的方式以Python源码的格式将翻译字段提取到了插件的源代码中。
1. 为了获得我们希望的可以用于编辑的POT文件还需要点击Export PO按钮在弹出的窗口中选择Ballance Blender Plugin保存的位置可以选择任意的文件夹例如桌面因为它会产生许多文件其中只有POT文件才是我们想要的取消勾选右侧的Update Existing选项并确保Export POT是勾选的最后进行保存。导出完成后可以在你选择的文件夹中找到一个名为`blender.pot`的翻译模板文件,以及众多以语言标识符为文件名的`.po`文件。
1. 你需要复制`blender.pot``bbp_ng/i18n`文件夹下,并将其重命名为`bbp_ng.pot`。至此我们提取了所有需要翻译的内容。
1. 你需要复制`blender.pot``i18n`文件夹下,并将其重命名为`bbp_ng.pot`。至此我们提取了所有需要翻译的内容。
### 合并翻译模板
现在`bbp_ng/i18n`文件夹下有两个POT文件分别代表了两部分提取的待翻译文本我们需要把他们合并起来。在`bbp_ng/i18n`文件夹执行`xgettext -o blender.pot bbp_ng.pot bme.pot`来进行合并。合并完成后的`bbp_ng/i18n/blender.pot`将用作翻译模板。
现在`i18n`文件夹下有两个POT文件分别代表了两部分提取的待翻译文本我们需要把他们合并起来。在`i18n`文件夹执行`xgettext -o blender.pot bbp_ng.pot bme.pot`来进行合并。合并完成后的`i18n/blender.pot`将用作翻译模板。
### 创建新语言翻译
@ -80,7 +98,7 @@ PO格式的翻译并不能被Blender识别因此在翻译完成后你还
1. 首先确保关闭了所有Blender进程否则插件会保持在加载状态对翻译元组变量的修改会不起作用。
1. 将插件中翻译元组变量`translations_tuple`的值改为空元组参见前文有关提交的注意事项。这一步操作的意图是让整个插件不存在翻译条目这样之后在使用Import PO功能的时候Blender的多语言插件就会认为PO文件中存储的所有字段都是要翻译的就不会出现只导入了一部分翻译的情况因为BME部分的翻译是后来合并入的
1. 打开Blender转到I18n Update Translation面板按照提取翻译模板时的操作方法在语言列表中仅选中需要翻译的语言。
1. 点击最下方一栏的Import PO按钮然后在弹出的窗口中选择Ballance Blender Plugin然后选择`bbp_ng/i18n`文件夹进行导入。这样我们就完成了将PO文件导入为Blender可识别的Python源码格式的操作。
1. 点击最下方一栏的Import PO按钮然后在弹出的窗口中选择Ballance Blender Plugin然后选择`i18n`文件夹进行导入。这样我们就完成了将PO文件导入为Blender可识别的Python源码格式的操作。
## 打包

View File

@ -19,7 +19,9 @@ BBP插件目前有2个设置需要配置。
请填写为Ballance的`Texture`目录插件将从此目录下调用外置贴图文件即Ballance原本带有的贴图文件。点击右侧的文件夹按钮可以浏览文件夹并选择。
这是关乎BBP是否正常运行的关键只有填写正确BBP才不会在运行中出错
该选项几乎是必填写的。如果不填写该项目则Virtools文件导入导出BME创建钢轨创建等各种核心功能将不可用按钮为灰色
该选项是BBP能否正常运行的关键只有填写正确BBP才不会在运行中出错。
### No Component Collection

View File

@ -39,7 +39,12 @@ Conflict Options冲突解决选项章节指示了当导入器遇到物体
### 导出目标
Export Target导出目标章节用于决定你需要将哪写物体导出到Virtools文档中。你可以选择导出一个集合或一个物体,并在下面选择对应的集合或物体。需要注意的是,选择集合的时候,会将内部集合中的物体也一起导出,即支持嵌套集合的导出。
Export Target导出目标章节用于决定你需要将哪写物体导出到Virtools文档中。你可以在四种模式中选择其一,来决定需要导出的内容:
* Object导出单个物体。选择后需要在下面选择需要导出的物体。
* Collection导出单个集合**这是最常用的选项**。选择后需要在下面选择需要导出的集合。值得注意的是,该选项支持嵌套集合导出,即选择集合的时候,会将集合中的集合的物体也一起导出。
* Selected Objects导出选择的物体。你需要在导出前选择好需要导出的物体。
* All Objects导出该文档中的所有物体。该选项慎用因为它是粗暴地遍历文档中的物体列表来进行导出很可能会导出许多你不需要的物体。
### Virtools参数
@ -56,3 +61,11 @@ Ballance ParamsBallance参数章节包含针对Ballance特有内容
Successive Sector小节连续是一个解决导出小节组时出现的Bug的选项。由于某些原因如果一个小节中没有任何机关实际上是某小节组中没有归入任何物体导出插件会认为该小节组不存在因而遗漏导出。且由于Ballance对最终小节即飞船出现小节的判定是从1开始递增寻找最后一个存在的小节组所以二者叠加会导致Ballance错误地认定地图的小节数从而在错误的小节显示飞船这也就是导出Bug。当勾选此选项后导出文档时会预先按照当前Blender文件中Ballance地图信息中指定的小节数预先创建所有小节组然后再进行导出这样就不会遗漏创建某些小节组飞船也会在正确的小节显示。
这个选项通常在导出可游玩的地图时选中,如果你只是想导出一些模型,那么需要关闭此选项,否则会在最终文件中产生许多无用的小节组。
## 无法导入导出
通常而言在你正确按照之前介绍的步骤安装和配置插件后你理论上就可以使用Virtools文档的导入导出功能了。但难免有意外发生。这里说的无法导入导出指的是导入和导出Virtools文档的按钮是灰色的不可点击的。你需要按照下述步骤检查。如果你所指的是在导入导出过程中出错了请参考本页页首的警告信息。
首先检查你是否已经在[配置插件](configure-plugin.md)章节正确配置了插件的`External Texture Folder`设置项。如果你没有配置则自然无法导入导出Virtools文件。因为导入导出Virtools文件需要依赖Ballance原版关卡数据。请按教程认真配置这一设置项。
如果你确定你已经设置了正确的贴图路径,你可以尝试点击菜单`Window - Toggle System Console`来打开控制台。在控制台中可能会有一些相关的错误信息输出在其中例如加载底层BMap库时失败等。加载底层BMap库失败通常只发生在一些罕见架构的机器上例如使用Snapdragon处理器的Windows系统等因为我们打包的插件中并未包含支持这些平台的Virtools文件底层读取库BMap。在面对这种情况时你有两个选择一是汇报给开发者等待开发者主动支持二是自行编译该库仅建议有丰富计算机知识的人这样做

View File

@ -14,9 +14,11 @@
在面板中,`Align Axis`指定了你要对齐的轴,此处可以多选以指定多个轴,不指定任何轴将无法进行对齐操作,因而也无法点击`Apply`按钮。
`Current Object`是对齐参考物体,也就是场景中的活动物体,通常也就是你选择的最后一个物体。在这个选项里指定你需要参考其什么数值进行对齐,分别有`Min`(轴上最小值)、`Center (Bounding Box)`(碰撞箱的中心)、`Center (Axis)`(物体的原点)、`Max`轴上的最大值可选。这些选项与3ds Max中的对齐选项是一致的
`Current Object`指示选择哪个实例作为对齐参考。你可以选择场景中的活动物体,通常也就是你选择的最后一个物体。或者是3D游标。需要注意的是如果你选择活动物体模式那么活动物体将排除在对齐操作之外不会被移动因为参考物体是不可动的。而如果你选择3D游标则活动物体会被纳入对齐操作的范围之中
`Target Objects`是正在被对齐的物体,可能有很多个,在这个选项里也是指定你需要参考其什么数值进行对齐。选项与`Current Object`含义一致。
`Current Object Align Mode`是对齐参考物体的对齐模式它只有在你选择活动物体作为对齐参考物体时才会出现。因为3D游标是一个单纯的点而物体占有一定体积我们需要按照某种模式后文叙述在空间中选择一个点作为后续对齐操作时使用的点。在这个选项里指定你需要参考其什么数值进行对齐,分别有`Min`(轴上最小值)、`Center (Bounding Box)`(碰撞箱的中心)、`Center (Axis)`(物体的原点)、`Max`轴上的最大值可选。这些选项与3ds Max中的对齐选项是一致
`Target Objects Align Mode`是正在被对齐的物体,可能有很多个,在这个选项里也是指定你需要参考其什么数值进行对齐。选项与`Current Object Align Mode`含义一致。
`Apply`按钮点击后将把当前页面的配置压入操作栈,并重置上面的设置,使得你可以开始新一轮对齐操作而无需再次执行传统对齐。操作栈中的操作个数在`Apply`按钮下方显示。

View File

@ -1,9 +1,13 @@
# 技术信息
## 标准与协议文档
* BM文件标准https://github.com/yyc12345/gist/blob/master/BMFileSpec/BMSpec_ZH.md
* 制图工具链标准及`meshes`文件夹下的文件的格式https://github.com/yyc12345/gist/blob/master/BMFileSpec/YYCToolsChainSpec_ZH.md
* BMERevenge的JSON文件的格式https://github.com/yyc12345/gist/blob/master/BMERevenge/DevDocument_v2.0_ZH.md
## 开发辅助包
本插件配合了`fake-bpy-module`模块来实现类型提示以加快开发速度。使用如下命令来安装Blender的类型提示库。
* Blender 3.6: `pip install fake-bpy-module-latest==20230627`
@ -11,3 +15,16 @@
* Blender 4.5: `pip install fake-bpy-module-latest==20250604`
这么做主要是因为`fake-bpy-module`没有很及时地发布适用于指定Blender版本的包因此我只能通过选择最接近Blender对应版本离开`main`主线时间的每日编译版本来安装它(因为每日编译版本只编译`main`主线)。
!!! question "为什么不采用Blender官方的bpy模块"
Blender在PyPI上提供了官方的名为`bpy`的包但我们不会采用它作为我们的开发辅助包。因为它基本上就是将Blender打包成了一个模块也就意味着你基本上又把Blender重新下载了一遍使得你可以通过Python来操纵Blender。这与我们使用一个仅提供类型提示的包来辅助插件开发的目的相悖。
## 版本号规则
BBP的版本号格式遵循[语义化版本](https://semver.org/lang/zh-CN/)。但略有区别:
* 主版本号只在重构整个插件时提升。
* 次版本号是常规更新使用。
* 修订号则是在不修改任何功能的情况下递增的版本号。例如4.2.1版本仅增加了对macOS Blender的更新不更改任何功能。
在BBP发布一个正式版前通常有3个阶段性版本分别是Alpha版本Beta版本和RC版本。Alpha版本专注于功能性更新用于检验新添加或修改的功能是否正常工作不包含文档和翻译。Beta版本则专注于插件文档而RC版本则关注于插件翻译。但这三个版本并非总是存在如果更新内容较少则可能会跳过其中一些版本或直接进行发布。

View File

@ -7,3 +7,31 @@
窥视归组并转换为网格其全称为窥视并复制曲线倒角物体的Virtools归组信息后再转换为网格。你可以选中一些物体后右键在物体上下文菜单中找到这一功能。
该功能正如其名,其将选中的物体转换为网格,如果选中的物体是曲线,且设置了倒角物体,则将倒角物体的归组信息赋予当前曲线(覆盖曲线当前归组设置)。如果选中的物体不是曲线,或者是曲线但没有倒角物体,那么该功能与执行转换为网格无异。该功能在放样建模时极为有用,因为只需要为截面物体进行正确的归组,然后再使用此功能将曲线转换为网格,就可以确保放样后的物体归组正确。
## 游戏内摄像机
许多制图者在制作地图时,往往对地图大小没有概念,很容易创建出过大或过小的地图。菜单项`Ballance - Game Camera`的游戏内摄像机功能则提供了一种在Blender内以游戏内摄像机视角预览地图的功能方便制图者可以把握地图的大小。
为了使用该功能,你的场景中必须首先有一个摄像机,且被设置为场景的活动摄像机(无摄像机时,通过添加菜单新添加的摄像机会被自动设置)。然后你还需要一个活动物体,且该活动物体不能是这个摄像机。
!!! question "为什么一定需要活动物体?"
由于Blender插件的限制活动物体是必须的因为游戏内摄像机功能支持以3D游标 **或活动物体** 为目标。
通常只有在一个空白的Blender文档中才会出现无活动物体可用的情况进而导致该功能不可用。对于一张自制地图最不缺少的就是可成为活动物体的物体只需要随便点击一个物体就可以得到当想使用3D游标作为目标时
点击该功能后,视图将自动转为活动摄像机的视角,方便你进行预览。你可以在左下角的面板中调整相关设置。
![](../imgs/game-camera.png)
首先是选择目标实际上就是选择玩家球位于哪里。如果选择3D游标则将3D游标视为玩家球。如果选择活动物体则将玩家球放置在活动物体的原点。
!!! info "玩家球的位置并非那么简单"
无论是选择3D游标还是选择活动物体如果不做特别精细的调整其预览的结果和游戏中会有细微出入。
当你使用3D游标模式并将其简单地吸附到地面上时它并非代表球的位置。因为玩家球是一个半径为2的球实际上你需要向+Z方向移动2个单位长度才能够得到和游戏中一模一样的视角。但这个操作过于繁琐不执行这个操作预览的效果也不会偏差太多。
当你使用活动物体模式时需要注意物体的原点是选择物体时显示的那个圆点的位置。对于大多数物体这可能并非你想要的因为他们可能位于物体内部或外部并非总位于物体表面或者某个面的中心。针对这种情况我建议你改用3D游标作为目标并通过进入编辑模式灵活运用吸附和`Shift + S`菜单,来将游标放置在正确的位置。
然后是选择摄像机的旋转角度。我们提供了8种游戏内预设角度分别对应90度和45度的各4种。此外如果这些预设角度不能满足你的需求你还可以设置自定义角度。
最后是选择摄像机视角分为Ordinary常规视角Lift按住空格键的视角和Easter Egg彩蛋视角三种。

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

View File

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

View File

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

View File

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