Compare commits
37 Commits
1044d4b450
...
master
Author | SHA1 | Date | |
---|---|---|---|
f920cdcaf5 | |||
40cad381af | |||
0d20a1fe44 | |||
599e9a6ef0 | |||
e22b888bfc | |||
88ef1d3202 | |||
f2af90c876 | |||
4dba3c3a71 | |||
e31a677d83 | |||
35fcbe54b5 | |||
9e83fe0a10 | |||
33fb1a65d3 | |||
415cc98758 | |||
2d93ce1340 | |||
1129872234 | |||
2b2b18cfa4 | |||
b19800e37f | |||
e14729500c | |||
48bfc54830 | |||
7e74e42bd7 | |||
96a81b165b | |||
0681f0d240 | |||
d700f1276a | |||
3bea3d67b9 | |||
ec41b7553a | |||
9e2539499e | |||
3a5cd1c937 | |||
1afa5f9893 | |||
1383e87104 | |||
a2b8f41a21 | |||
93f23abeb9 | |||
4ba3ff9e5a | |||
a9a889a8fd | |||
fc34b19a42 | |||
9e65d258d7 | |||
ab266a07fb | |||
61c7709b97 |
2
.gitattributes
vendored
@ -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
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 42 KiB |
@ -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": [
|
@ -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": [
|
@ -1,4 +1,12 @@
|
||||
[
|
||||
// One of Chris suggested more vanilla prototypes.
|
||||
// This prototype represent a half of a normal, sink or double ribbon border,
|
||||
// which looks like trapezoid from top to bottom.
|
||||
//
|
||||
// The bottom edge of trapezoid is from origin to +X with `long_edge_length` length.
|
||||
// The length of top edge is `short_edge_length` and it just like moving bottom edge to +Y direction.
|
||||
// The offset between top edge and bottom edge is always 2.5.
|
||||
// The distance from the closest point of top edge, to Y axis is `short_edge_offset`.
|
||||
{
|
||||
"identifier": "cv_trapezoid_side",
|
||||
"showcase": null,
|
||||
@ -153,6 +161,11 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
// Same as previous one, but looks like triangle from top to bottom.
|
||||
//
|
||||
// The bottom edge is from origin to +X with `edge_length` length.
|
||||
// The tip is going to +Y.
|
||||
// The height of this triangle is always 2.5 and the offset between tip and Y axis is `tip_offset`.
|
||||
{
|
||||
"identifier": "cv_triangle_side",
|
||||
"showcase": null,
|
@ -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": [
|
@ -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": [
|
@ -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"
|
@ -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": [
|
@ -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": [
|
@ -1,4 +1,5 @@
|
||||
[
|
||||
// The shared template prototype used by all floor terminals.
|
||||
{
|
||||
"identifier": "raw_floor_terminal",
|
||||
"showcase": null,
|
||||
@ -22,26 +23,30 @@
|
||||
"faces": [],
|
||||
"instances": [
|
||||
{
|
||||
"identifier": "cv_triangle_side",
|
||||
"identifier": "cv_trapezoid_side",
|
||||
"skip": "False",
|
||||
"params": {
|
||||
"edge_length": "2.5",
|
||||
"tip_offset": "2.5",
|
||||
"long_edge_length": "5.0",
|
||||
"short_edge_offset": "2.5",
|
||||
"short_edge_length": "2.5",
|
||||
"height": "height",
|
||||
"face": "(face[0], False, False, face[3], face[4], None)",
|
||||
"is_sink": "is_sink"
|
||||
"face": "(face[0], False, False, face[3], face[4], False)",
|
||||
"is_sink": "is_sink",
|
||||
"is_ribbon": "False"
|
||||
},
|
||||
"transform": "ident()"
|
||||
},
|
||||
{
|
||||
"identifier": "cv_triangle_side",
|
||||
"identifier": "cv_trapezoid_side",
|
||||
"skip": "False",
|
||||
"params": {
|
||||
"edge_length": "2.5",
|
||||
"tip_offset": "2.5",
|
||||
"long_edge_length": "5.0",
|
||||
"short_edge_offset": "2.5",
|
||||
"short_edge_length": "2.5",
|
||||
"height": "height",
|
||||
"face": "(face[0], False, False, face[3], face[5], None)",
|
||||
"is_sink": "is_sink"
|
||||
"face": "(face[0], False, False, face[3], face[5], False)",
|
||||
"is_sink": "is_sink",
|
||||
"is_ribbon": "False"
|
||||
},
|
||||
"transform": "move(0, 5, 0) @ scale(1, -1, 1)"
|
||||
},
|
||||
@ -61,7 +66,7 @@
|
||||
"identifier": "floor_rectangle_bottom",
|
||||
"skip": "not face[1]",
|
||||
"params": {
|
||||
"length": "2.5",
|
||||
"length": "5",
|
||||
"width": "5"
|
||||
},
|
||||
"transform": "move(0, 0, -height)"
|
||||
@ -72,6 +77,7 @@
|
||||
"identifier": "floor_normal_terminal",
|
||||
"showcase": {
|
||||
"title": "Normal Floor Terminal",
|
||||
"category": "Floors",
|
||||
"icon": "NormalFloorTerminal",
|
||||
"type": "floor",
|
||||
"cfgs": [
|
||||
@ -122,6 +128,7 @@
|
||||
"identifier": "floor_sink_terminal",
|
||||
"showcase": {
|
||||
"title": "Sink Floor Terminal",
|
||||
"category": "Floors",
|
||||
"icon": "SinkFloorTerminal",
|
||||
"type": "floor",
|
||||
"cfgs": [
|
@ -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": [
|
@ -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": [
|
@ -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": [
|
||||
@ -166,34 +168,50 @@
|
||||
"transform": "rot(0, 0, 90) @ scale(1, -1, 1)"
|
||||
},
|
||||
{
|
||||
"identifier": "cv_triangle_side",
|
||||
"identifier": "cv_trapezoid_side",
|
||||
"skip": "False",
|
||||
"params": {
|
||||
"edge_length": "2.5",
|
||||
"tip_offset": "2.5",
|
||||
"long_edge_length": "5.0",
|
||||
"short_edge_offset": "2.5",
|
||||
"short_edge_length": "2.5",
|
||||
"height": "height",
|
||||
"face": "(face[0], False, False, face[3], face[4], None)",
|
||||
"is_sink": "True"
|
||||
"face": "(face[0], False, False, face[3], face[4], False)",
|
||||
"is_sink": "True",
|
||||
"is_ribbon": "False"
|
||||
},
|
||||
"transform": "ident()"
|
||||
},
|
||||
{
|
||||
"identifier": "cv_triangle_side",
|
||||
"identifier": "cv_trapezoid_side",
|
||||
"skip": "False",
|
||||
"params": {
|
||||
"edge_length": "2.5",
|
||||
"tip_offset": "2.5",
|
||||
"long_edge_length": "5.0",
|
||||
"short_edge_offset": "2.5",
|
||||
"short_edge_length": "2.5",
|
||||
"height": "height",
|
||||
"face": "(face[0], False, False, face[3], face[5], None)",
|
||||
"is_sink": "True"
|
||||
"face": "(face[0], False, False, face[3], face[5], False)",
|
||||
"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"
|
||||
},
|
||||
"transform": "move(2.5, 2.5, 0)"
|
||||
},
|
||||
{
|
||||
"identifier": "floor_rectangle_bottom",
|
||||
"skip": "not face[1]",
|
||||
"params": {
|
||||
"length": "2.5",
|
||||
"length": "5",
|
||||
"width": "5 + width"
|
||||
},
|
||||
"transform": "move(0, 0, -height)"
|
||||
@ -204,6 +222,7 @@
|
||||
"identifier": "floor_wide_l_crossing",
|
||||
"showcase": {
|
||||
"title": "Wide Floor L Crossing",
|
||||
"category": "Wide Floors",
|
||||
"icon": "WideLCrossing",
|
||||
"type": "floor",
|
||||
"cfgs": [
|
||||
@ -336,6 +355,7 @@
|
||||
"identifier": "floor_wide_t_crossing",
|
||||
"showcase": {
|
||||
"title": "Wide Floor T Crossing",
|
||||
"category": "Wide Floors",
|
||||
"icon": "WideTCrossing",
|
||||
"type": "floor",
|
||||
"cfgs": [
|
||||
@ -459,6 +479,7 @@
|
||||
"identifier": "floor_wide_x_crossing",
|
||||
"showcase": {
|
||||
"title": "Wide Floor X Crossing",
|
||||
"category": "Wide Floors",
|
||||
"icon": "WideXCrossing",
|
||||
"type": "floor",
|
||||
"cfgs": [
|
@ -5,7 +5,7 @@ from . import UTIL_functions, UTIL_translation, UTIL_bme
|
||||
|
||||
#region BME Adder
|
||||
|
||||
_g_EnumHelper_BmeStructType: UTIL_bme.EnumPropHelper = UTIL_bme.EnumPropHelper()
|
||||
_g_EnumHelper_BmeStructType = UTIL_bme.EnumPropHelper()
|
||||
|
||||
class BBP_PG_bme_adder_cfgs(bpy.types.PropertyGroup):
|
||||
prop_int: bpy.props.IntProperty(
|
||||
@ -37,39 +37,44 @@ class BBP_OT_add_bme_struct(bpy.types.Operator):
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
bl_translation_context = 'BBP_OT_add_bme_struct'
|
||||
|
||||
## There is a compromise due to the shitty Blender design.
|
||||
#
|
||||
# The passed `self` of Blender Property update function is not the instance of operator,
|
||||
# but a simple OperatorProperties.
|
||||
# It mean that I can not visit the full operator, only what I can do is visit existing
|
||||
# Blender properties.
|
||||
#
|
||||
# So these is the solution about generating cache list according to the change of bme struct type.
|
||||
# First, update function will only set a "outdated" flag for operator which is a pre-registered Blender property.
|
||||
# The "outdated" flags is not showen and not saved.
|
||||
# Then call a internal cache list update function at the begin of `invoke`, `execute` and `draw`.
|
||||
# In this internal cache list updator, check "outdated" flag first, if cache is outdated, update and reset flag.
|
||||
# Otherwise do nothing.
|
||||
#
|
||||
# Reference: https://docs.blender.org/api/current/bpy.props.html#update-example
|
||||
|
||||
## Compromise used "outdated" flag.
|
||||
outdated_flag: bpy.props.BoolProperty(
|
||||
# TR: Property not showen should not have name and desc.
|
||||
# name = "Outdated Type",
|
||||
# description = "Internal flag.",
|
||||
options = {'HIDDEN', 'SKIP_SAVE'},
|
||||
default = False
|
||||
) # type: ignore
|
||||
# YYC MARK:
|
||||
# ===== 20231217 =====
|
||||
# There is a compromise due to the shitty Blender design.
|
||||
# The passed `self` of Blender Property update function is not the instance of operator,
|
||||
# but a simple OperatorProperties.
|
||||
# It mean that I can not visit the full operator, only what I can do is visit existing
|
||||
# Blender properties.
|
||||
#
|
||||
# So these is the solution about generating cache list according to the change of bme struct type.
|
||||
# First, update function will only set a "outdated" flag for operator which is a pre-registered Blender property.
|
||||
# The "outdated" flags is not showen and not saved.
|
||||
# Then call a internal cache list update function at the begin of `invoke`, `execute` and `draw`.
|
||||
# In this internal cache list updator, check "outdated" flag first, if cache is outdated, update and reset flag.
|
||||
# Otherwise do nothing.
|
||||
#
|
||||
# Reference: https://docs.blender.org/api/current/bpy.props.html#update-example
|
||||
#
|
||||
# ===== 20250131 =====
|
||||
# There is a fatal performance bug when I adding BME operator list into 3D View sidebar panels (N Menu).
|
||||
# It will cause calling my Panel's `draw` function infinityly of Panel in each render tick,
|
||||
# which calls `BBP_OT_add_bme_struct.draw_blc_menu` directly,
|
||||
# eat too much CPU and GPU resources and make the whole Blender be laggy.
|
||||
#
|
||||
# After some research, I found that if I comment the parameter `update` of the member `bme_struct_type`,
|
||||
# everything will be resolved.
|
||||
# It even doesn't work that do nothing in update function.
|
||||
# So I realize that sidebar panel may not be compatible with update function.
|
||||
# After reading the note written above, I decide to remove the whole feature of this ugly implementation,
|
||||
# so that I need to remove the ability that changing BME prototype type in left-bottom window.
|
||||
#
|
||||
# After talking with the requestor of this feature, ZZQ,
|
||||
# he agree with my decision and I think this change will not broke any experience of BBP.
|
||||
|
||||
## A BME struct cfgs descriptor cache list
|
||||
# Not only the descriptor self, also the cfg associated index in bme_struct_cfgs
|
||||
bme_struct_cfg_index_cache: list[tuple[UTIL_bme.PrototypeShowcaseCfgDescriptor, int]]
|
||||
|
||||
def __internal_update_bme_struct_type(self) -> None:
|
||||
# if not outdated, skip
|
||||
if not self.outdated_flag: return
|
||||
|
||||
def __build_bme_struct_cfg_index_cache(self) -> None:
|
||||
# get available cfg entires
|
||||
cfgs: typing.Iterator[UTIL_bme.PrototypeShowcaseCfgDescriptor]
|
||||
cfgs = _g_EnumHelper_BmeStructType.get_bme_showcase_cfgs(
|
||||
@ -125,21 +130,10 @@ class BBP_OT_add_bme_struct(bpy.types.Operator):
|
||||
for i in range(6):
|
||||
op_cfgs_visitor[cfg_index + i].prop_bool = default_values[i]
|
||||
|
||||
# reset outdated flag
|
||||
self.outdated_flag = False
|
||||
|
||||
# the updator for default side value
|
||||
def bme_struct_type_updated(self, context):
|
||||
# update outdated flag
|
||||
self.outdated_flag = True
|
||||
# blender required
|
||||
return None
|
||||
|
||||
bme_struct_type: bpy.props.EnumProperty(
|
||||
name = "Type",
|
||||
description = "The type of BME structure.",
|
||||
items = _g_EnumHelper_BmeStructType.generate_items(),
|
||||
update = bme_struct_type_updated,
|
||||
translation_context = 'BBP_OT_add_bme_struct/property'
|
||||
) # type: ignore
|
||||
|
||||
@ -180,19 +174,16 @@ class BBP_OT_add_bme_struct(bpy.types.Operator):
|
||||
self.extra_translation = (0.0, 0.0, 0.0)
|
||||
self.extra_rotation = (0.0, 0.0, 0.0)
|
||||
self.extra_scale = (1.0, 1.0, 1.0)
|
||||
|
||||
# create internal list
|
||||
self.bme_struct_cfg_index_cache = []
|
||||
# trigger default bme struct type updator
|
||||
self.bme_struct_type_updated(context)
|
||||
# call internal updator
|
||||
self.__internal_update_bme_struct_type()
|
||||
# call internal builder to load prototype data inside it
|
||||
self.__build_bme_struct_cfg_index_cache()
|
||||
|
||||
# run execute() function
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
# call internal updator
|
||||
self.__internal_update_bme_struct_type()
|
||||
|
||||
# create cfg visitor
|
||||
op_cfgs_visitor: UTIL_functions.CollectionVisitor[BBP_PG_bme_adder_cfgs]
|
||||
op_cfgs_visitor = UTIL_functions.CollectionVisitor(self.bme_struct_cfgs)
|
||||
@ -231,13 +222,8 @@ class BBP_OT_add_bme_struct(bpy.types.Operator):
|
||||
return {'FINISHED'}
|
||||
|
||||
def draw(self, context):
|
||||
# call internal updator
|
||||
self.__internal_update_bme_struct_type()
|
||||
|
||||
# start drawing
|
||||
layout: bpy.types.UILayout = self.layout
|
||||
# show type
|
||||
layout.prop(self, 'bme_struct_type')
|
||||
|
||||
# create cfg visitor
|
||||
op_cfgs_visitor: UTIL_functions.CollectionVisitor[BBP_PG_bme_adder_cfgs]
|
||||
@ -264,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(
|
||||
@ -294,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
|
||||
|
||||
|
@ -176,7 +176,7 @@ class _GeneralComponentCreator():
|
||||
|
||||
#endregion
|
||||
|
||||
#region Noemal Component Adder
|
||||
#region Normal Component Adder
|
||||
|
||||
# element enum prop helper
|
||||
|
||||
@ -184,7 +184,7 @@ def _get_component_icon_by_name(elename: str):
|
||||
icon: int | None = UTIL_icons_manager.get_component_icon(elename)
|
||||
if icon is None: return UTIL_icons_manager.get_empty_icon()
|
||||
else: return icon
|
||||
_g_EnumHelper_Component: UTIL_functions.EnumPropHelper = UTIL_functions.EnumPropHelper(
|
||||
_g_EnumHelper_Component = UTIL_functions.EnumPropHelper(
|
||||
PROP_ballance_element.BallanceElementType,
|
||||
lambda x: str(x.value),
|
||||
lambda x: PROP_ballance_element.BallanceElementType(int(x)),
|
||||
@ -217,7 +217,7 @@ class BBP_OT_add_component(bpy.types.Operator, ComponentSectorParam):
|
||||
layout.prop(self, "component_type")
|
||||
|
||||
# only show sector for non-PE/PS component
|
||||
eletype: PROP_ballance_element.BallanceElementType = _g_EnumHelper_Component.get_selection(self.component_type)
|
||||
eletype = _g_EnumHelper_Component.get_selection(self.component_type)
|
||||
if eletype != PROP_ballance_element.BallanceElementType.PS_FourFlames and eletype != PROP_ballance_element.BallanceElementType.PE_Balloon:
|
||||
self.draw_component_sector_params(layout)
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
474
bbp_ng/OP_OBJECT_game_view.py
Normal file
@ -0,0 +1,474 @@
|
||||
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(
|
||||
# YYC MAKR:
|
||||
# This property is not shown on UI layout,
|
||||
# but it should be translated because it is not PURE assistant property.
|
||||
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(
|
||||
name = "Preset Rotation Angle",
|
||||
description = "",
|
||||
translation_context = 'BBP_OT_game_camera/property',
|
||||
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)
|
||||
|
@ -9,19 +9,35 @@ class AlignMode(enum.IntEnum):
|
||||
BBoxCenter = enum.auto()
|
||||
AxisCenter = enum.auto()
|
||||
Max = enum.auto()
|
||||
_g_AlignModeDesc: dict[AlignMode, tuple[str, str]] = {
|
||||
AlignMode.Min: ("Min", "The min value in specified axis."),
|
||||
AlignMode.BBoxCenter: ("Center (Bounding Box)", "The bounding box center in specified axis."),
|
||||
AlignMode.AxisCenter: ("Center (Axis)", "The object's source point in specified axis."),
|
||||
AlignMode.Max: ("Max", "The max value in specified axis."),
|
||||
_g_AlignModeDesc: dict[AlignMode, tuple[str, str, str]] = {
|
||||
AlignMode.Min: ("Min", "The min value in specified axis.", "REMOVE"),
|
||||
AlignMode.BBoxCenter: ("Center (Bounding Box)", "The bounding box center in specified axis.", "SHADING_BBOX"),
|
||||
AlignMode.AxisCenter: ("Center (Axis)", "The object's source point in specified axis.", "OBJECT_ORIGIN"),
|
||||
AlignMode.Max: ("Max", "The max value in specified axis.", "ADD"),
|
||||
}
|
||||
_g_EnumHelper_AlignMode: UTIL_functions.EnumPropHelper = UTIL_functions.EnumPropHelper(
|
||||
_g_EnumHelper_AlignMode = UTIL_functions.EnumPropHelper(
|
||||
AlignMode,
|
||||
lambda x: str(x.value),
|
||||
lambda x: AlignMode(int(x)),
|
||||
lambda x: _g_AlignModeDesc[x][0],
|
||||
lambda x: _g_AlignModeDesc[x][1],
|
||||
lambda _: ''
|
||||
lambda x: _g_AlignModeDesc[x][2]
|
||||
)
|
||||
|
||||
class CurrentInstance(enum.IntEnum):
|
||||
ActiveObject = enum.auto()
|
||||
Cursor = enum.auto()
|
||||
_g_CurrentInstanceDesc: dict[CurrentInstance, tuple[str, str, str]] = {
|
||||
CurrentInstance.ActiveObject: ("Active Object", "Use Active Object as Current Object", "OBJECT_DATA"),
|
||||
CurrentInstance.Cursor: ("3D Cursor", "Use 3D Cursor as Current Object", "CURSOR"),
|
||||
}
|
||||
_g_EnumHelper_CurrentInstance = UTIL_functions.EnumPropHelper(
|
||||
CurrentInstance,
|
||||
lambda x: str(x.value),
|
||||
lambda x: CurrentInstance(int(x)),
|
||||
lambda x: _g_CurrentInstanceDesc[x][0],
|
||||
lambda x: _g_CurrentInstanceDesc[x][1],
|
||||
lambda x: _g_CurrentInstanceDesc[x][2]
|
||||
)
|
||||
|
||||
#endregion
|
||||
@ -55,14 +71,23 @@ class BBP_PG_legacy_align_history(bpy.types.PropertyGroup):
|
||||
default = False,
|
||||
translation_context = 'BBP_PG_legacy_align_history/property'
|
||||
) # type: ignore
|
||||
current_instance: bpy.props.EnumProperty(
|
||||
name = "Current Instance",
|
||||
description = "Decide which instance should be used as Current Object",
|
||||
items = _g_EnumHelper_CurrentInstance.generate_items(),
|
||||
default = _g_EnumHelper_CurrentInstance.to_selection(CurrentInstance.ActiveObject),
|
||||
translation_context = 'BBP_PG_legacy_align_history/property'
|
||||
) # type: ignore
|
||||
current_align_mode: bpy.props.EnumProperty(
|
||||
name = "Current Object (Active Object)",
|
||||
name = "Current Object",
|
||||
description = "The align mode applied to Current Object",
|
||||
items = _g_EnumHelper_AlignMode.generate_items(),
|
||||
default = _g_EnumHelper_AlignMode.to_selection(AlignMode.AxisCenter),
|
||||
translation_context = 'BBP_PG_legacy_align_history/property'
|
||||
) # type: ignore
|
||||
target_align_mode: bpy.props.EnumProperty(
|
||||
name = "Target Objects (Selected Objects)",
|
||||
name = "Target Objects",
|
||||
description = "The align mode applied to Target Objects (selected objects except active object if Current Instance is active object)",
|
||||
items = _g_EnumHelper_AlignMode.generate_items(),
|
||||
default = _g_EnumHelper_AlignMode.to_selection(AlignMode.AxisCenter),
|
||||
translation_context = 'BBP_PG_legacy_align_history/property'
|
||||
@ -112,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'},
|
||||
@ -120,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
|
||||
@ -148,7 +173,7 @@ class BBP_OT_legacy_align(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
# get processed objects
|
||||
(current_obj, target_objs) = _prepare_objects()
|
||||
(current_obj, current_cursor, target_objs) = _prepare_objects()
|
||||
# YYC MARK:
|
||||
# This statement is VERY IMPORTANT.
|
||||
# If this statement is not presented, Blender will return identity matrix
|
||||
@ -162,7 +187,8 @@ class BBP_OT_legacy_align(bpy.types.Operator):
|
||||
histories = UTIL_functions.CollectionVisitor(self.align_history)
|
||||
for entry in histories:
|
||||
_align_objects(
|
||||
current_obj, target_objs,
|
||||
_g_EnumHelper_CurrentInstance.get_selection(entry.current_instance),
|
||||
current_obj, current_cursor, target_objs,
|
||||
entry.align_x, entry.align_y, entry.align_z,
|
||||
_g_EnumHelper_AlignMode.get_selection(entry.current_align_mode),
|
||||
_g_EnumHelper_AlignMode.get_selection(entry.target_align_mode)
|
||||
@ -185,11 +211,22 @@ class BBP_OT_legacy_align(bpy.types.Operator):
|
||||
row.prop(entry, "align_y", toggle = 1)
|
||||
row.prop(entry, "align_z", toggle = 1)
|
||||
|
||||
# show mode
|
||||
# show current instance
|
||||
col.separator()
|
||||
col.label(text='Current Object (Active Object)', 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='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)
|
||||
|
||||
# show instance and mode
|
||||
col.separator()
|
||||
# only show current object mode if current instance is active object,
|
||||
# because there is no mode for 3d cursor.
|
||||
current_instnce = _g_EnumHelper_CurrentInstance.get_selection(entry.current_instance)
|
||||
if current_instnce == CurrentInstance.ActiveObject:
|
||||
col.label(text='Current Object Align Mode', text_ctxt='BBP_OT_legacy_align/draw')
|
||||
col.prop(entry, "current_align_mode", expand = True)
|
||||
col.label(text='Target Objects Align Mode', text_ctxt='BBP_OT_legacy_align/draw')
|
||||
col.prop(entry, "target_align_mode", expand = True)
|
||||
|
||||
# show apply button
|
||||
@ -206,44 +243,66 @@ class BBP_OT_legacy_align(bpy.types.Operator):
|
||||
#region Core Functions
|
||||
|
||||
def _check_align_requirement() -> bool:
|
||||
# if we are not in object mode, do not do legacy align
|
||||
# If we are not in object mode, do not do legacy align
|
||||
if not UTIL_functions.is_in_object_mode():
|
||||
return False
|
||||
|
||||
# check current obj
|
||||
# YYC MARK:
|
||||
# We still need to check active object (as current object)
|
||||
# although we can choose align with active object or 3d cursor.
|
||||
# Because we can not make any promise that user will
|
||||
# select Active Object or 3D Cursor as current object before executing this operator.
|
||||
if bpy.context.active_object is None:
|
||||
return False
|
||||
# check target obj with filter of current obj
|
||||
length = len(bpy.context.selected_objects)
|
||||
if bpy.context.active_object in bpy.context.selected_objects:
|
||||
length -= 1
|
||||
return length != 0
|
||||
|
||||
# YYC MARK:
|
||||
# Roughly check selected objects.
|
||||
# We do not need exclude active object from selected objects,
|
||||
# because active object may be moved when 3D Cursor is current object.
|
||||
if len(bpy.context.selected_objects) == 0:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _prepare_objects() -> tuple[bpy.types.Object, set[bpy.types.Object]]:
|
||||
# get current object
|
||||
current_obj: bpy.types.Object = bpy.context.active_object
|
||||
def _prepare_objects() -> tuple[bpy.types.Object, mathutils.Vector, list[bpy.types.Object]]:
|
||||
# Fetch current object
|
||||
current_obj = typing.cast(bpy.types.Object, bpy.context.active_object)
|
||||
|
||||
# get target objects
|
||||
target_objs: set[bpy.types.Object] = set(bpy.context.selected_objects)
|
||||
# remove active one
|
||||
if current_obj in target_objs:
|
||||
target_objs.remove(current_obj)
|
||||
# Fetch 3d cursor location
|
||||
current_cursor: mathutils.Vector = bpy.context.scene.cursor.location
|
||||
|
||||
# YYC MARK:
|
||||
# Fetch target objects and do NOT remove active object from it.
|
||||
# because active object will be moved when current instance is 3D Cursor.
|
||||
target_objs: list[bpy.types.Object] = bpy.context.selected_objects[:]
|
||||
|
||||
# return value
|
||||
return (current_obj, target_objs)
|
||||
return (current_obj, current_cursor, target_objs)
|
||||
|
||||
def _align_objects(
|
||||
current_obj: bpy.types.Object, target_objs: set[bpy.types.Object],
|
||||
current_instance: CurrentInstance,
|
||||
current_obj: bpy.types.Object, current_cursor: mathutils.Vector, target_objs: list[bpy.types.Object],
|
||||
align_x: bool, align_y: bool, align_z: bool, current_mode: AlignMode, target_mode: AlignMode) -> None:
|
||||
# if no align, skip
|
||||
if not (align_x or align_y or align_z):
|
||||
return
|
||||
|
||||
# calc current object data
|
||||
current_obj_ref: mathutils.Vector = _get_object_ref_point(current_obj, current_mode)
|
||||
current_obj_ref: mathutils.Vector
|
||||
match current_instance:
|
||||
case CurrentInstance.ActiveObject:
|
||||
current_obj_ref = _get_object_ref_point(current_obj, current_mode)
|
||||
case CurrentInstance.Cursor:
|
||||
current_obj_ref = current_cursor
|
||||
|
||||
# process each target obj
|
||||
for target_obj in target_objs:
|
||||
# YYC MARK:
|
||||
# If we use active object as current instance, we need exclude it from target objects,
|
||||
# because there is no pre-exclude considering the scenario that 3D Cursor is current instance.
|
||||
if current_instance == CurrentInstance.ActiveObject and current_obj == target_obj:
|
||||
continue
|
||||
|
||||
# calc target object data
|
||||
target_obj_ref: mathutils.Vector = _get_object_ref_point(target_obj, target_mode)
|
||||
# build translation transform
|
||||
@ -256,21 +315,21 @@ def _align_objects(
|
||||
# apply translation transform to left side (add into original matrix)
|
||||
target_obj.matrix_world = target_obj_translation_matrix @ target_obj.matrix_world
|
||||
|
||||
bpy.context.scene.update_tag
|
||||
|
||||
def _get_object_ref_point(obj: bpy.types.Object, mode: AlignMode) -> mathutils.Vector:
|
||||
ref_pos: mathutils.Vector = mathutils.Vector((0, 0, 0))
|
||||
ref_pos = mathutils.Vector((0, 0, 0))
|
||||
|
||||
# calc bounding box data
|
||||
corners: tuple[mathutils.Vector] = tuple(obj.matrix_world @ mathutils.Vector(corner) for corner in obj.bound_box)
|
||||
bbox_min_corner: mathutils.Vector = mathutils.Vector((0, 0, 0))
|
||||
bbox_min_corner.x = min((vec.x for vec in corners))
|
||||
bbox_min_corner.y = min((vec.y for vec in corners))
|
||||
bbox_min_corner.z = min((vec.z for vec in corners))
|
||||
bbox_max_corner: mathutils.Vector = mathutils.Vector((0, 0, 0))
|
||||
bbox_max_corner.x = max((vec.x for vec in corners))
|
||||
bbox_max_corner.y = max((vec.y for vec in corners))
|
||||
bbox_max_corner.z = max((vec.z for vec in corners))
|
||||
corners: tuple[mathutils.Vector, ...] = tuple(obj.matrix_world @ mathutils.Vector(corner) for corner in obj.bound_box)
|
||||
bbox_min_corner = mathutils.Vector((
|
||||
min((vec.x for vec in corners)),
|
||||
min((vec.y for vec in corners)),
|
||||
min((vec.z for vec in corners)),
|
||||
))
|
||||
bbox_max_corner = mathutils.Vector((
|
||||
max((vec.x for vec in corners)),
|
||||
max((vec.y for vec in corners)),
|
||||
max((vec.z for vec in corners)),
|
||||
))
|
||||
|
||||
# return value by given align mode
|
||||
match(mode):
|
||||
|
@ -18,7 +18,7 @@ _g_SelectModeDesc: dict[SelectMode, tuple[str, str, str]] = {
|
||||
SelectMode.Difference: ('Invert', 'Inverts the selection.', 'SELECT_DIFFERENCE'),
|
||||
SelectMode.Intersect: ('Intersect', 'Selects items that intersect with the existing selection.', 'SELECT_INTERSECT')
|
||||
}
|
||||
_g_EnumHelper_SelectMode: UTIL_functions.EnumPropHelper = UTIL_functions.EnumPropHelper(
|
||||
_g_EnumHelper_SelectMode = UTIL_functions.EnumPropHelper(
|
||||
SelectMode,
|
||||
lambda x: str(x.value),
|
||||
lambda x: SelectMode(int(x)),
|
||||
|
@ -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)
|
||||
|
@ -205,7 +205,7 @@ class VirtoolsGroupsPreset(enum.Enum):
|
||||
|
||||
Shadow = "Shadow"
|
||||
|
||||
_g_VtGrpPresetValues: tuple[str] = tuple(map(lambda x: x.value, VirtoolsGroupsPreset))
|
||||
_g_VtGrpPresetValues: tuple[str, ...] = tuple(map(lambda x: x.value, VirtoolsGroupsPreset))
|
||||
|
||||
## Some of group names are not matched with icon name
|
||||
# So we create a convertion map to convert them.
|
||||
@ -236,7 +236,7 @@ def _get_group_icon_by_name(gp_name: str) -> int:
|
||||
if value is not None: return value
|
||||
else: return UTIL_icons_manager.get_empty_icon()
|
||||
# blender group name prop helper
|
||||
_g_EnumHelper_Group: UTIL_functions.EnumPropHelper = UTIL_functions.EnumPropHelper(
|
||||
_g_EnumHelper_Group = UTIL_functions.EnumPropHelper(
|
||||
VirtoolsGroupsPreset,
|
||||
lambda x: x.value, # member is string self
|
||||
lambda x: VirtoolsGroupsPreset(x), # convert directly because it is StrEnum.
|
||||
|
@ -73,7 +73,7 @@ class RawVirtoolsLight():
|
||||
|
||||
# Blender Property Group
|
||||
|
||||
_g_Helper_VXLIGHT_TYPE: UTIL_virtools_types.EnumPropHelper = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXLIGHT_TYPE)
|
||||
_g_Helper_VXLIGHT_TYPE = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXLIGHT_TYPE)
|
||||
|
||||
class BBP_PG_virtools_light(bpy.types.PropertyGroup):
|
||||
light_type: bpy.props.EnumProperty(
|
||||
|
@ -114,13 +114,13 @@ class RawVirtoolsMaterial():
|
||||
|
||||
#region Blender Enum Prop Helper (Virtools type specified)
|
||||
|
||||
_g_Helper_VXTEXTURE_BLENDMODE: UTIL_virtools_types.EnumPropHelper = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXTEXTURE_BLENDMODE)
|
||||
_g_Helper_VXTEXTURE_FILTERMODE: UTIL_virtools_types.EnumPropHelper = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXTEXTURE_FILTERMODE)
|
||||
_g_Helper_VXTEXTURE_ADDRESSMODE: UTIL_virtools_types.EnumPropHelper = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXTEXTURE_ADDRESSMODE)
|
||||
_g_Helper_VXBLEND_MODE: UTIL_virtools_types.EnumPropHelper = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXBLEND_MODE)
|
||||
_g_Helper_VXFILL_MODE: UTIL_virtools_types.EnumPropHelper = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXFILL_MODE)
|
||||
_g_Helper_VXSHADE_MODE: UTIL_virtools_types.EnumPropHelper = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXSHADE_MODE)
|
||||
_g_Helper_VXCMPFUNC: UTIL_virtools_types.EnumPropHelper = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXCMPFUNC)
|
||||
_g_Helper_VXTEXTURE_BLENDMODE = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXTEXTURE_BLENDMODE)
|
||||
_g_Helper_VXTEXTURE_FILTERMODE = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXTEXTURE_FILTERMODE)
|
||||
_g_Helper_VXTEXTURE_ADDRESSMODE = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXTEXTURE_ADDRESSMODE)
|
||||
_g_Helper_VXBLEND_MODE = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXBLEND_MODE)
|
||||
_g_Helper_VXFILL_MODE = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXFILL_MODE)
|
||||
_g_Helper_VXSHADE_MODE = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXSHADE_MODE)
|
||||
_g_Helper_VXCMPFUNC = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXCMPFUNC)
|
||||
|
||||
#endregion
|
||||
|
||||
@ -558,7 +558,7 @@ def preset_virtools_material(mtl: bpy.types.Material, preset_type: MaterialPrese
|
||||
set_raw_virtools_material(mtl, preset_data)
|
||||
|
||||
# create preset enum blender helper
|
||||
_g_Helper_MtlPreset: UTIL_functions.EnumPropHelper = UTIL_functions.EnumPropHelper(
|
||||
_g_Helper_MtlPreset = UTIL_functions.EnumPropHelper(
|
||||
MaterialPresetType,
|
||||
lambda x: str(x.value),
|
||||
lambda x: MaterialPresetType(int(x)),
|
||||
@ -572,13 +572,13 @@ _g_Helper_MtlPreset: UTIL_functions.EnumPropHelper = UTIL_functions.EnumPropHelp
|
||||
#region Fix Material
|
||||
|
||||
def fix_material(mtl: bpy.types.Material) -> bool:
|
||||
"""!
|
||||
"""
|
||||
Fix single Blender material.
|
||||
|
||||
@remark The implementation of this function is copied from BallanceVirtoolsHelper/bvh/features/mapping/bmfile_fix_texture.cpp
|
||||
The implementation of this function is copied from `BallanceVirtoolsHelper/bvh/features/mapping/bmfile_fix_texture.cpp`
|
||||
|
||||
@param mtl[in] The blender material need to be processed.
|
||||
@return True if we do a fix, otherwise return False.
|
||||
:param mtl: The blender material need to be processed.
|
||||
:return: True if we do a fix, otherwise return False.
|
||||
"""
|
||||
# prepare return value first
|
||||
ret: bool = False
|
||||
@ -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
|
||||
|
@ -15,7 +15,7 @@ class RawVirtoolsMesh():
|
||||
self.mLitMode = kwargs.get('mLitMode', RawVirtoolsMesh.cDefaultLitMode)
|
||||
|
||||
# blender enum prop helper defines
|
||||
_g_Helper_VXMESH_LITMODE: UTIL_virtools_types.EnumPropHelper = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXMESH_LITMODE)
|
||||
_g_Helper_VXMESH_LITMODE = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXMESH_LITMODE)
|
||||
|
||||
# Blender Property Group
|
||||
|
||||
|
@ -20,8 +20,8 @@ class RawVirtoolsTexture():
|
||||
self.mVideoFormat = kwargs.get('mVideoFormat', RawVirtoolsTexture.cDefaultVideoFormat)
|
||||
|
||||
# blender enum prop helper defines
|
||||
_g_Helper_CK_TEXTURE_SAVEOPTIONS: UTIL_virtools_types.EnumPropHelper = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.CK_TEXTURE_SAVEOPTIONS)
|
||||
_g_Helper_VX_PIXELFORMAT: UTIL_virtools_types.EnumPropHelper = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VX_PIXELFORMAT)
|
||||
_g_Helper_CK_TEXTURE_SAVEOPTIONS = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.CK_TEXTURE_SAVEOPTIONS)
|
||||
_g_Helper_VX_PIXELFORMAT = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VX_PIXELFORMAT)
|
||||
|
||||
class BBP_PG_virtools_texture(bpy.types.PropertyGroup):
|
||||
|
||||
@ -64,12 +64,10 @@ def set_raw_virtools_texture(img: bpy.types.Image, rawdata: RawVirtoolsTexture)
|
||||
|
||||
#region Virtools Texture Drawer
|
||||
|
||||
"""!
|
||||
@remark
|
||||
Because Image do not have its unique properties window
|
||||
so we only can draw virtools texture properties in other window
|
||||
we provide various function to help draw property.
|
||||
"""
|
||||
# YYC MARK:
|
||||
# Because Image do not have its unique properties window,
|
||||
# so we only can draw Virtools Texture properties in other window.
|
||||
# We provide various functions to help draw properties.
|
||||
|
||||
def draw_virtools_texture(img: bpy.types.Image, layout: bpy.types.UILayout):
|
||||
props: BBP_PG_virtools_texture = get_virtools_texture(img)
|
||||
|
@ -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,
|
||||
@ -187,15 +188,38 @@ class PrototypeShowcaseCfgDescriptor():
|
||||
def get_default(self) -> typing.Any:
|
||||
return _eval_showcase_cfgs_default(self.__mRawCfg[TOKEN_SHOWCASE_CFGS_DEFAULT])
|
||||
|
||||
class EnumPropHelper(UTIL_functions.EnumPropHelper):
|
||||
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
|
||||
UTIL_functions.EnumPropHelper.__init__(
|
||||
self,
|
||||
super().__init__(
|
||||
self.get_bme_identifiers(),
|
||||
lambda x: x,
|
||||
lambda x: x,
|
||||
@ -203,17 +227,20 @@ class EnumPropHelper(UTIL_functions.EnumPropHelper):
|
||||
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.
|
||||
@ -327,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] = {}
|
||||
@ -352,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.
|
||||
@ -367,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,
|
||||
|
@ -2,37 +2,35 @@ import bpy, mathutils
|
||||
import math, typing, enum, sys
|
||||
|
||||
class BBPException(Exception):
|
||||
"""
|
||||
The exception thrown by Ballance Blender Plugin
|
||||
"""
|
||||
""" The exception thrown by Ballance Blender Plugin"""
|
||||
pass
|
||||
|
||||
def clamp_float(v: float, min_val: float, max_val: float) -> float:
|
||||
"""!
|
||||
@brief Clamp a float value
|
||||
"""
|
||||
Clamp a float value
|
||||
|
||||
@param v[in] The value need to be clamp.
|
||||
@param min_val[in] The allowed minium value, including self.
|
||||
@param max_val[in] The allowed maxium value, including self.
|
||||
@return Clamped value.
|
||||
:param v: The value need to be clamp.
|
||||
:param min_val: The allowed minium value (inclusive).
|
||||
:param max_val: The allowed maxium value (inclusive).
|
||||
:return: Clamped value.
|
||||
"""
|
||||
if (max_val < min_val): raise BBPException("Invalid range of clamp_float().")
|
||||
|
||||
|
||||
if (v < min_val): return min_val
|
||||
elif (v > max_val): return max_val
|
||||
else: return v
|
||||
|
||||
def clamp_int(v: int, min_val: int, max_val: int) -> int:
|
||||
"""!
|
||||
@brief Clamp a int value
|
||||
"""
|
||||
Clamp a int value
|
||||
|
||||
@param v[in] The value need to be clamp.
|
||||
@param min_val[in] The allowed minium value, including self.
|
||||
@param max_val[in] The allowed maxium value, including self.
|
||||
@return Clamped value.
|
||||
:param v: The value need to be clamp.
|
||||
:param min_val: The allowed minium value (inclusive).
|
||||
:param max_val: The allowed maxium value (inclusive).
|
||||
:return: Clamped value.
|
||||
"""
|
||||
if (max_val < min_val): raise BBPException("Invalid range of clamp_int().")
|
||||
|
||||
|
||||
if (v < min_val): return min_val
|
||||
elif (v > max_val): return max_val
|
||||
else: return v
|
||||
@ -41,41 +39,65 @@ def message_box(message: tuple[str, ...], title: str, icon: str):
|
||||
"""
|
||||
Show a message box in Blender. Non-block mode.
|
||||
|
||||
@param message[in] The text this message box displayed. Each item in this param will show as a single line.
|
||||
@param title[in] Message box title text.
|
||||
@param icon[in] The icon this message box displayed.
|
||||
:param message: The text this message box displayed. Each item in this param will show as a single line.
|
||||
:param title: Message box title text.
|
||||
:param icon: The icon this message box displayed.
|
||||
"""
|
||||
def draw(self, context: bpy.types.Context):
|
||||
layout = self.layout
|
||||
for item in message:
|
||||
layout.label(text=item, translate=False)
|
||||
|
||||
|
||||
bpy.context.window_manager.popup_menu(draw, title = title, icon = icon)
|
||||
|
||||
def add_into_scene(obj: bpy.types.Object):
|
||||
"""
|
||||
Add given object into active scene.
|
||||
|
||||
:param obj: The 3d object to be added.
|
||||
"""
|
||||
view_layer = bpy.context.view_layer
|
||||
collection = view_layer.active_layer_collection.collection
|
||||
collection.objects.link(obj)
|
||||
|
||||
def move_to_cursor(obj: bpy.types.Object):
|
||||
# use obj.matrix_world to move, not obj.location because this bug:
|
||||
# https://blender.stackexchange.com/questions/27667/incorrect-matrix-world-after-transformation
|
||||
# the update of matrix_world after setting location is not immediately.
|
||||
# and calling update() function for view_layer for the translation of each object is not suit for too much objects.
|
||||
"""
|
||||
Move given object to the position of cursor.
|
||||
|
||||
:param obj: The 3d object to be moved.
|
||||
"""
|
||||
# YYC MARK:
|
||||
# Use `obj.matrix_world` to move, not `obj.location`, because this bug:
|
||||
# https://blender.stackexchange.com/questions/27667/incorrect-matrix-world-after-transformation
|
||||
# The update of `matrix_world` after setting `location` is not immediately.
|
||||
# And it is inviable that calling `update()` function for `view_layer` to update these fields,
|
||||
# because it involve too much objects and cost too much time.
|
||||
|
||||
# obj.location = bpy.context.scene.cursor.location
|
||||
obj.matrix_world = obj.matrix_world @ mathutils.Matrix.Translation(bpy.context.scene.cursor.location - obj.location)
|
||||
|
||||
def add_into_scene_and_move_to_cursor(obj: bpy.types.Object):
|
||||
"""
|
||||
Add given object into active scene and move it to cursor position.
|
||||
|
||||
This function is just a simple combination of previous functions.
|
||||
|
||||
:param obj: The 3d object to be processed.
|
||||
"""
|
||||
add_into_scene(obj)
|
||||
move_to_cursor(obj)
|
||||
|
||||
def select_certain_objects(objs: tuple[bpy.types.Object, ...]) -> None:
|
||||
"""
|
||||
Deselect all objects and then select given 3d objects.
|
||||
|
||||
:param objs: The tuple of 3d objects to be selected.
|
||||
"""
|
||||
# deselect all objects first
|
||||
bpy.ops.object.select_all(action = 'DESELECT')
|
||||
# if no objects, return
|
||||
if len(objs) == 0: return
|
||||
|
||||
|
||||
# set selection for each object
|
||||
for obj in objs:
|
||||
obj.select_set(True)
|
||||
@ -83,66 +105,79 @@ def select_certain_objects(objs: tuple[bpy.types.Object, ...]) -> None:
|
||||
bpy.context.view_layer.objects.active = objs[0]
|
||||
|
||||
def is_in_object_mode() -> bool:
|
||||
"""
|
||||
Check whether we are in Blender Object Mode.
|
||||
|
||||
:return: True if we are in object mode which suit for exporting something.
|
||||
"""
|
||||
# get active object from context
|
||||
obj = bpy.context.active_object
|
||||
|
||||
|
||||
# if there is no active object, we think it is in object mode
|
||||
if obj is None: return True
|
||||
|
||||
|
||||
# simply check active object mode
|
||||
return obj.mode == 'OBJECT'
|
||||
|
||||
class EnumPropHelper():
|
||||
#region Blender Enum Property Helper
|
||||
|
||||
_TRawEnum = typing.TypeVar('_TRawEnum')
|
||||
|
||||
_TFctToStr = typing.Callable[[_TRawEnum], str]
|
||||
_TFctFromStr = typing.Callable[[str], _TRawEnum]
|
||||
_TFctName = typing.Callable[[_TRawEnum], str]
|
||||
_TFctDesc = typing.Callable[[_TRawEnum], str]
|
||||
_TFctIcon = typing.Callable[[_TRawEnum], str | int]
|
||||
|
||||
class EnumPropHelper(typing.Generic[_TRawEnum]):
|
||||
"""
|
||||
These class contain all functions related to EnumProperty, including generating `items`,
|
||||
parsing data from EnumProperty string value and getting EnumProperty acceptable string format from data.
|
||||
"""
|
||||
|
||||
# YYC MARK:
|
||||
# I don't know why I can have subscripting for a `typing.Callable` object.
|
||||
# It was not introduced in any document and I just know it from AI.
|
||||
# If I am not doing this, the type hint will crash into Unknown type.
|
||||
# But it works now I don't want to touch it anymore.
|
||||
|
||||
# define some type hint
|
||||
_TFctToStr = typing.Callable[[typing.Any], str]
|
||||
_TFctFromStr = typing.Callable[[str], typing.Any]
|
||||
_TFctName = typing.Callable[[typing.Any], str]
|
||||
_TFctDesc = typing.Callable[[typing.Any], str]
|
||||
_TFctIcon = typing.Callable[[typing.Any], str | int]
|
||||
|
||||
# define class member
|
||||
|
||||
__mCollections: typing.Iterable[typing.Any]
|
||||
__mFctToStr: _TFctToStr
|
||||
__mFctFromStr: _TFctFromStr
|
||||
__mFctName: _TFctName
|
||||
__mFctDesc: _TFctDesc
|
||||
__mFctIcon: _TFctIcon
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
collections_: typing.Iterable[typing.Any],
|
||||
fct_to_str: _TFctToStr,
|
||||
fct_from_str: _TFctFromStr,
|
||||
fct_name: _TFctName,
|
||||
fct_desc: _TFctDesc,
|
||||
fct_icon: _TFctIcon):
|
||||
__mCollections: typing.Iterable[_TRawEnum]
|
||||
__mFctToStr: _TFctToStr[_TRawEnum]
|
||||
__mFctFromStr: _TFctFromStr[_TRawEnum]
|
||||
__mFctName: _TFctName[_TRawEnum]
|
||||
__mFctDesc: _TFctDesc[_TRawEnum]
|
||||
__mFctIcon: _TFctIcon[_TRawEnum]
|
||||
|
||||
def __init__(self, collections: typing.Iterable[_TRawEnum],
|
||||
fct_to_str: _TFctToStr[_TRawEnum], fct_from_str: _TFctFromStr[_TRawEnum],
|
||||
fct_name: _TFctName[_TRawEnum], fct_desc: _TFctDesc[_TRawEnum],
|
||||
fct_icon: _TFctIcon[_TRawEnum]):
|
||||
"""
|
||||
Initialize a EnumProperty helper.
|
||||
Initialize an EnumProperty helper.
|
||||
|
||||
@param collections_ [in] The collection all available enum property entries contained.
|
||||
It can be enum.Enum or a simple list/tuple/dict.
|
||||
@param fct_to_str [in] A function pointer converting data collection member to its string format.
|
||||
For enum.IntEnum, it can be simply `lambda x: str(x.value)`
|
||||
@param fct_from_str [in] A function pointer getting data collection member from its string format.
|
||||
For enum.IntEnum, it can be simply `lambda x: TEnum(int(x))`
|
||||
@param fct_name [in] A function pointer converting data collection member to its display name.
|
||||
@param fct_desc [in] Same as `fct_name` but return description instead. Return empty string, not None if no description.
|
||||
@param fct_icon [in] Same as `fct_name` but return the used icon instead. Return empty string if no icon.
|
||||
:param collections: The collection containing all available enum property entries.
|
||||
It can be `enum.Enum` or a simple list/tuple.
|
||||
:param fct_to_str: A function pointer converting data collection member to its string format.
|
||||
You must make sure that each members built name is unique in collection!
|
||||
For `enum.IntEnum`, it can be simple `lambda x: str(x.value)`
|
||||
:param fct_from_str: A function pointer getting data collection member from its string format.
|
||||
This class promise that given string must can be parsed.
|
||||
For `enum.IntEnum`, it can be simple `lambda x: TEnum(int(x))`
|
||||
:param fct_name: A function pointer converting data collection member to its display name which shown in Blender.
|
||||
:param fct_desc: Same as `fct_name` but return description instead which shown in Blender
|
||||
If no description, return empty string, not None.
|
||||
:param fct_icon: Same as `fct_name` but return the used icon instead which shown in Blender.
|
||||
It can be a Blender builtin icon string, or any loaded icon integer ID.
|
||||
If no icon, return empty string.
|
||||
"""
|
||||
# assign member
|
||||
self.__mCollections = collections_
|
||||
self.__mCollections = collections
|
||||
self.__mFctToStr = fct_to_str
|
||||
self.__mFctFromStr = fct_from_str
|
||||
self.__mFctName = fct_name
|
||||
self.__mFctDesc = fct_desc
|
||||
self.__mFctIcon = fct_icon
|
||||
|
||||
|
||||
def generate_items(self) -> tuple[tuple[str, str, str, int | str, int], ...]:
|
||||
"""
|
||||
Generate a tuple which can be applied to Blender EnumProperty's "items".
|
||||
@ -152,27 +187,29 @@ class EnumPropHelper():
|
||||
return tuple(
|
||||
(
|
||||
self.__mFctToStr(member), # call to_str as its token.
|
||||
self.__mFctName(member),
|
||||
self.__mFctDesc(member),
|
||||
self.__mFctIcon(member),
|
||||
self.__mFctName(member),
|
||||
self.__mFctDesc(member),
|
||||
self.__mFctIcon(member),
|
||||
idx # use hardcode index, not the collection member self.
|
||||
) for idx, member in enumerate(self.__mCollections)
|
||||
)
|
||||
|
||||
def get_selection(self, prop: str) -> typing.Any:
|
||||
def get_selection(self, prop: str) -> _TRawEnum:
|
||||
"""
|
||||
Return collection member from given Blender EnumProp string data.
|
||||
"""
|
||||
# call from_str fct ptr
|
||||
return self.__mFctFromStr(prop)
|
||||
|
||||
def to_selection(self, val: typing.Any) -> str:
|
||||
def to_selection(self, val: _TRawEnum) -> str:
|
||||
"""
|
||||
Parse collection member to Blender EnumProp acceptable string format.
|
||||
"""
|
||||
# call to_str fct ptr
|
||||
return self.__mFctToStr(val)
|
||||
|
||||
#endregion
|
||||
|
||||
#region Blender Collection Visitor
|
||||
|
||||
_TPropertyGroup = typing.TypeVar('_TPropertyGroup', bound = bpy.types.PropertyGroup)
|
||||
@ -183,40 +220,43 @@ class CollectionVisitor(typing.Generic[_TPropertyGroup]):
|
||||
Blender collcetion property lack essential type hint and document.
|
||||
So I create a wrapper for my personal use to reduce type hint errors raised by my linter.
|
||||
"""
|
||||
|
||||
|
||||
__mSrcProp: bpy.types.CollectionProperty
|
||||
|
||||
|
||||
def __init__(self, src_prop: bpy.types.CollectionProperty):
|
||||
self.__mSrcProp = src_prop
|
||||
|
||||
|
||||
def add(self) -> _TPropertyGroup:
|
||||
"""!
|
||||
@brief Adds a new item to the collection.
|
||||
@return The instance of newly created item.
|
||||
"""
|
||||
Adds a new item to the collection.
|
||||
|
||||
:return: The instance of newly created item.
|
||||
"""
|
||||
return self.__mSrcProp.add()
|
||||
|
||||
|
||||
def remove(self, index: int) -> None:
|
||||
"""!
|
||||
@brief Removes the item at the specified index from the collection.
|
||||
@param[in] index The index of the item to remove.
|
||||
"""
|
||||
Removes the item at the specified index from the collection.
|
||||
|
||||
:param index: The index of the item to remove.
|
||||
"""
|
||||
self.__mSrcProp.remove(index)
|
||||
|
||||
|
||||
def move(self, from_index: int, to_index: int) -> None:
|
||||
"""!
|
||||
@brief Moves an item from one index to another within the collection.
|
||||
@param[in] from_index The current index of the item to move.
|
||||
@param[in] to_index The target index where the item should be moved.
|
||||
"""
|
||||
Moves an item from one index to another within the collection.
|
||||
|
||||
:param from_index: The current index of the item to move.
|
||||
:param to_index: The target index where the item should be moved.
|
||||
"""
|
||||
self.__mSrcProp.move(from_index, to_index)
|
||||
|
||||
|
||||
def clear(self) -> None:
|
||||
"""!
|
||||
@brief Clears all items from the collection.
|
||||
"""
|
||||
Clears all items from the collection.
|
||||
"""
|
||||
self.__mSrcProp.clear()
|
||||
|
||||
|
||||
def __len__(self) -> int:
|
||||
return self.__mSrcProp.__len__()
|
||||
def __getitem__(self, index: int | str) -> _TPropertyGroup:
|
||||
@ -238,32 +278,50 @@ _TMutexObject = typing.TypeVar('_TMutexObject')
|
||||
|
||||
class TinyMutex(typing.Generic[_TMutexObject]):
|
||||
"""
|
||||
In this plugin, some class have "with" context feature.
|
||||
However, it is essential to block any futher visiting if some "with" context are operating on some object.
|
||||
In this plugin, some classes have "with" context feature.
|
||||
However, in some cases, it is essential to block any futher visiting if some "with" context are operating on some object.
|
||||
This is the reason why this tiny mutex is designed.
|
||||
|
||||
Please note this class is not a real MUTEX.
|
||||
We just want to make sure the resources only can be visited by one "with" context.
|
||||
So it doesn't matter that we do not use lock before operating something.
|
||||
"""
|
||||
|
||||
|
||||
__mProtectedObjects: set[_TMutexObject]
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self.__mProtectedObjects = set()
|
||||
|
||||
|
||||
def lock(self, obj: _TMutexObject) -> None:
|
||||
"""
|
||||
Lock given object.
|
||||
|
||||
:raise BBPException: Raised if given object has been locked.
|
||||
:param obj: The resource to be locked.
|
||||
"""
|
||||
if obj in self.__mProtectedObjects:
|
||||
raise BBPException('It is not allowed that operate multiple "with" contexts on a single object.')
|
||||
self.__mProtectedObjects.add(obj)
|
||||
|
||||
|
||||
def try_lock(self, obj: _TMutexObject) -> bool:
|
||||
"""
|
||||
Try lock given object.
|
||||
|
||||
:param obj: The resource to be locked.
|
||||
:return: True if we successfully lock it, otherwise false.
|
||||
"""
|
||||
if obj in self.__mProtectedObjects:
|
||||
return False
|
||||
self.__mProtectedObjects.add(obj)
|
||||
return True
|
||||
|
||||
|
||||
def unlock(self, obj: _TMutexObject) -> None:
|
||||
"""
|
||||
Unlock given object.
|
||||
|
||||
:raise BBPException: Raised if given object is not locked.
|
||||
:param obj: The resource to be unlocked.
|
||||
"""
|
||||
if obj not in self.__mProtectedObjects:
|
||||
raise BBPException('It is not allowed that unlock an non-existent object.')
|
||||
self.__mProtectedObjects.remove(obj)
|
||||
|
@ -3,11 +3,13 @@ import enum, typing
|
||||
from . import UTIL_virtools_types, UTIL_functions
|
||||
from . import PROP_ptrprop_resolver, PROP_ballance_map_info
|
||||
|
||||
## Intent
|
||||
# Some importer or exporter may share same properties.
|
||||
# So we create some shared class and user just need inherit them
|
||||
# and call general getter to get user selected data.
|
||||
# Also provide draw function thus caller do not need draw the params themselves.
|
||||
# INTENT:
|
||||
# Some importer or exporter may share same properties.
|
||||
# So we create some shared class and user just need inherit them
|
||||
# and call general getter to get user selected data.
|
||||
# Also provide draw function thus caller do not need draw the params themselves.
|
||||
|
||||
#region Import Params
|
||||
|
||||
class ConflictStrategy(enum.IntEnum):
|
||||
Rename = enum.auto()
|
||||
@ -16,7 +18,7 @@ _g_ConflictStrategyDesc: dict[ConflictStrategy, tuple[str, str]] = {
|
||||
ConflictStrategy.Rename: ('Rename', 'Rename the new one'),
|
||||
ConflictStrategy.Current: ('Use Current', 'Use current one'),
|
||||
}
|
||||
_g_EnumHelper_ConflictStrategy: UTIL_functions.EnumPropHelper = UTIL_functions.EnumPropHelper(
|
||||
_g_EnumHelper_ConflictStrategy = UTIL_functions.EnumPropHelper(
|
||||
ConflictStrategy,
|
||||
lambda x: str(x.value),
|
||||
lambda x: ConflictStrategy(int(x)),
|
||||
@ -25,39 +27,6 @@ _g_EnumHelper_ConflictStrategy: UTIL_functions.EnumPropHelper = UTIL_functions.E
|
||||
lambda _: ''
|
||||
)
|
||||
|
||||
#region Assist Classes
|
||||
|
||||
class ExportEditModeBackup():
|
||||
"""
|
||||
The class which save Edit Mode when exporting and restore it after exporting.
|
||||
Because edit mode is not allowed when exporting.
|
||||
Support `with` statement.
|
||||
|
||||
```
|
||||
with ExportEditModeBackup():
|
||||
# do some exporting work
|
||||
blabla()
|
||||
# restore automatically when exiting "with"
|
||||
```
|
||||
"""
|
||||
mInEditMode: bool
|
||||
|
||||
def __init__(self):
|
||||
if bpy.context.object and bpy.context.object.mode == "EDIT":
|
||||
# set and toggle it. otherwise exporting will failed.
|
||||
self.mInEditMode = True
|
||||
bpy.ops.object.editmode_toggle()
|
||||
else:
|
||||
self.mInEditMode = False
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
if self.mInEditMode:
|
||||
bpy.ops.object.editmode_toggle()
|
||||
self.mInEditMode = False
|
||||
|
||||
class ConflictResolver():
|
||||
"""
|
||||
This class frequently used when importing objects.
|
||||
@ -151,8 +120,6 @@ class ConflictResolver():
|
||||
tex.name = name
|
||||
return (tex, True)
|
||||
|
||||
#endregion
|
||||
|
||||
class ImportParams():
|
||||
texture_conflict_strategy: bpy.props.EnumProperty(
|
||||
name = "Texture Name Conflict",
|
||||
@ -239,13 +206,67 @@ class ImportParams():
|
||||
self.general_get_texture_conflict_strategy()
|
||||
)
|
||||
|
||||
#endregion
|
||||
|
||||
#region Export Params
|
||||
|
||||
class ExportEditModeBackup():
|
||||
"""
|
||||
The class which save Edit Mode when exporting and restore it after exporting.
|
||||
Because edit mode is not allowed when exporting.
|
||||
Support `with` statement.
|
||||
|
||||
```
|
||||
with ExportEditModeBackup():
|
||||
# do some exporting work
|
||||
blabla()
|
||||
# restore automatically when exiting "with"
|
||||
```
|
||||
"""
|
||||
mInEditMode: bool
|
||||
|
||||
def __init__(self):
|
||||
if bpy.context.object and bpy.context.object.mode == "EDIT":
|
||||
# set and toggle it. otherwise exporting will failed.
|
||||
self.mInEditMode = True
|
||||
bpy.ops.object.editmode_toggle()
|
||||
else:
|
||||
self.mInEditMode = False
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
if self.mInEditMode:
|
||||
bpy.ops.object.editmode_toggle()
|
||||
self.mInEditMode = False
|
||||
|
||||
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,
|
||||
lambda x: str(x.value),
|
||||
lambda x: ExportMode(int(x)),
|
||||
lambda x: _g_ExportModeDesc[x][0],
|
||||
lambda x: _g_ExportModeDesc[x][1],
|
||||
lambda x: _g_ExportModeDesc[x][2]
|
||||
)
|
||||
|
||||
class ExportParams():
|
||||
export_mode: bpy.props.EnumProperty(
|
||||
name = "Export Mode",
|
||||
items = (
|
||||
('COLLECTION', "Collection", "Export a collection", 'OUTLINER_COLLECTION', 0),
|
||||
('OBJECT', "Object", "Export an object", 'OBJECT_DATA', 1),
|
||||
),
|
||||
description = "Define which 3D objects should be exported",
|
||||
items = _g_EnumHelper_ExportMode.generate_items(),
|
||||
default = _g_EnumHelper_ExportMode.to_selection(ExportMode.BldColl),
|
||||
translation_context = 'BBP/UTIL_ioport_shared.ExportParams/property'
|
||||
) # type: ignore
|
||||
|
||||
@ -256,35 +277,48 @@ 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)
|
||||
ptrprops = PROP_ptrprop_resolver.PropsVisitor(context.scene)
|
||||
if self.export_mode == 'COLLECTION':
|
||||
ptrprops.draw_export_collection(body)
|
||||
elif self.export_mode == 'OBJECT':
|
||||
ptrprops.draw_export_object(body)
|
||||
match export_mode:
|
||||
case ExportMode.BldColl:
|
||||
ptrprops.draw_export_collection(body)
|
||||
case ExportMode.BldObj:
|
||||
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:
|
||||
def general_get_export_objects(self, context: bpy.types.Context) -> tuple[bpy.types.Object, ...] | None:
|
||||
"""
|
||||
Return resolved exported objects or None if no selection.
|
||||
"""
|
||||
export_mode = _g_EnumHelper_ExportMode.get_selection(self.export_mode)
|
||||
ptrprops = PROP_ptrprop_resolver.PropsVisitor(context.scene)
|
||||
if self.export_mode == 'COLLECTION':
|
||||
col: bpy.types.Collection = ptrprops.get_export_collection()
|
||||
if col is None: return None
|
||||
else:
|
||||
return tuple(col.all_objects)
|
||||
else:
|
||||
obj: bpy.types.Object = ptrprops.get_export_object()
|
||||
if obj is None: return None
|
||||
else: return (obj, )
|
||||
match export_mode:
|
||||
case ExportMode.BldColl:
|
||||
col: bpy.types.Collection = ptrprops.get_export_collection()
|
||||
if col is None: return None
|
||||
else: return tuple(col.all_objects)
|
||||
case ExportMode.BldObj:
|
||||
obj: bpy.types.Object = ptrprops.get_export_object()
|
||||
if obj is None: return None
|
||||
else: return (obj, )
|
||||
case ExportMode.BldSelObjs:
|
||||
return tuple(context.selected_objects)
|
||||
case ExportMode.BldAllObjs:
|
||||
return tuple(bpy.data.objects)
|
||||
|
||||
#endregion
|
||||
|
||||
#region Virtools Params
|
||||
|
||||
# define global tex save opt blender enum prop helper
|
||||
_g_EnumHelper_CK_TEXTURE_SAVEOPTIONS: UTIL_virtools_types.EnumPropHelper = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.CK_TEXTURE_SAVEOPTIONS)
|
||||
_g_EnumHelper_CK_TEXTURE_SAVEOPTIONS = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.CK_TEXTURE_SAVEOPTIONS)
|
||||
|
||||
class VirtoolsParams():
|
||||
texture_save_opt: bpy.props.EnumProperty(
|
||||
@ -310,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
|
||||
@ -334,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)
|
||||
@ -349,6 +390,10 @@ class VirtoolsParams():
|
||||
def general_get_compress_level(self) -> int:
|
||||
return self.compress_level
|
||||
|
||||
#endregion
|
||||
|
||||
#region Ballance Params
|
||||
|
||||
class BallanceParams():
|
||||
successive_sector: bpy.props.BoolProperty(
|
||||
name="Successive Sector",
|
||||
@ -387,3 +432,5 @@ class BallanceParams():
|
||||
map_info: PROP_ballance_map_info.RawBallanceMapInfo
|
||||
map_info = PROP_ballance_map_info.get_raw_ballance_map_info(bpy.context.scene)
|
||||
return map_info.mSectorCount
|
||||
|
||||
#endregion
|
||||
|
@ -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
|
||||
|
||||
|
@ -235,21 +235,28 @@ _g_Annotation: dict[type, dict[int, EnumAnnotation]] = {
|
||||
}
|
||||
}
|
||||
|
||||
class EnumPropHelper(UTIL_functions.EnumPropHelper):
|
||||
_TRawEnum = typing.TypeVar('_TRawEnum', bound = enum.Enum)
|
||||
|
||||
class EnumPropHelper(UTIL_functions.EnumPropHelper[_TRawEnum]):
|
||||
"""
|
||||
Virtools type specified Blender EnumProp helper.
|
||||
"""
|
||||
__mAnnotationDict: dict[int, EnumAnnotation]
|
||||
__mEnumTy: type[enum.Enum]
|
||||
__mEnumTy: type[_TRawEnum]
|
||||
|
||||
def __init__(self, ty: type[enum.Enum]):
|
||||
def __init__(self, ty: type[_TRawEnum]):
|
||||
# set enum type and annotation ref first
|
||||
self.__mEnumTy = ty
|
||||
self.__mAnnotationDict = _g_Annotation[ty]
|
||||
# init parent data
|
||||
UTIL_functions.EnumPropHelper.__init__(
|
||||
self,
|
||||
self.__mEnumTy, # enum.Enum it self is iterable
|
||||
|
||||
# YYC MARK:
|
||||
# It seems that Pylance has bad generic analyse ability in there.
|
||||
# It can not deduce the correct generic type in lambda.
|
||||
# I gave up.
|
||||
|
||||
# Init parent data
|
||||
super().__init__(
|
||||
self.__mEnumTy, # enum.Enum its self is iterable
|
||||
lambda x: str(x.value), # convert enum.Enum's value to string
|
||||
lambda x: self.__mEnumTy(int(x)), # use stored enum type and int() to get enum member
|
||||
lambda x: self.__mAnnotationDict[x.value].mDisplayName,
|
||||
@ -265,11 +272,11 @@ def virtools_name_regulator(name: str | None) -> str:
|
||||
if name: return name
|
||||
else: return bpy.app.translations.pgettext_data('annoymous', 'BME/UTIL_virtools_types.virtools_name_regulator()')
|
||||
|
||||
## Default Encoding for PyBMap
|
||||
# Use semicolon split each encodings. Support Western European and Simplified Chinese in default.
|
||||
# Since LibCmo 0.2, the encoding name of LibCmo become universal encoding which is platfoorm independent.
|
||||
# So no need set it according to different platform.
|
||||
# Use universal encoding name (like Python).
|
||||
# YYC MARK:
|
||||
# There are default encodings for PyBMap. We support Western European and Simplified Chinese in default.
|
||||
# Since LibCmo 0.2, the encoding name of LibCmo become universal encoding which is platfoorm independent.
|
||||
# So no need set it according to different platform.
|
||||
# Use universal encoding name (like Python).
|
||||
g_PyBMapDefaultEncodings: tuple[str, ...] = (
|
||||
'cp1252',
|
||||
'gbk'
|
||||
|
@ -1,8 +1,8 @@
|
||||
#region Reload and Import
|
||||
#region Import and Reload
|
||||
|
||||
# import core lib
|
||||
import bpy
|
||||
import typing, collections
|
||||
import typing, enum
|
||||
|
||||
# reload if needed
|
||||
# TODO: finish reload feature if needed.
|
||||
@ -10,8 +10,6 @@ import typing, collections
|
||||
if "bpy" in locals():
|
||||
import importlib
|
||||
|
||||
#endregion
|
||||
|
||||
# we must load icons manager first
|
||||
# and register it
|
||||
from . import UTIL_icons_manager
|
||||
@ -25,32 +23,72 @@ 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
|
||||
|
||||
#region Menu
|
||||
#endregion
|
||||
|
||||
# ===== Menu Defines =====
|
||||
#region Menu and Sidebar Panel
|
||||
|
||||
class BBP_MT_View3DMenu(bpy.types.Menu):
|
||||
"""Ballance 3D related operators"""
|
||||
bl_idname = "BBP_MT_View3DMenu"
|
||||
bl_label = "Ballance"
|
||||
bl_translation_context = 'BBP_MT_View3DMenu'
|
||||
#region Ballance Adder Menu and Panel
|
||||
|
||||
def draw(self, context):
|
||||
layout = 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)
|
||||
layout.separator()
|
||||
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='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()
|
||||
layout.label(text='Material', icon='MATERIAL', text_ctxt='BBP_MT_View3DMenu/draw')
|
||||
layout.operator(OP_MTL_fix_materials.BBP_OT_fix_all_materials.bl_idname)
|
||||
class DrawTarget(enum.IntEnum):
|
||||
BldMenu = enum.auto()
|
||||
BldPanel = enum.auto()
|
||||
|
||||
def reuse_create_layout(layout: bpy.types.UILayout, target: DrawTarget) -> bpy.types.UILayout:
|
||||
# If we are draw for Panel, we need use Grid to use space enough.
|
||||
match target:
|
||||
case DrawTarget.BldMenu:
|
||||
return layout
|
||||
case DrawTarget.BldPanel:
|
||||
return layout.grid_flow(even_columns=True, even_rows=True)
|
||||
|
||||
def reuse_draw_add_bme(layout: bpy.types.UILayout, target: DrawTarget):
|
||||
# Draw operators.
|
||||
OP_ADDS_bme.BBP_OT_add_bme_struct.draw_blc_menu(reuse_create_layout(layout, target))
|
||||
|
||||
def reuse_draw_add_rail(layout: bpy.types.UILayout, target: DrawTarget):
|
||||
layout.label(text="Sections", icon='MESH_CIRCLE', text_ctxt='BBP/__init__.reuse_draw_add_rail()')
|
||||
sublayout = reuse_create_layout(layout, target)
|
||||
sublayout.operator(OP_ADDS_rail.BBP_OT_add_rail_section.bl_idname)
|
||||
sublayout.operator(OP_ADDS_rail.BBP_OT_add_transition_section.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.label(text="Straight Rails", icon='IPO_CONSTANT', text_ctxt='BBP/__init__.reuse_draw_add_rail()')
|
||||
sublayout = reuse_create_layout(layout, target)
|
||||
sublayout.operator(OP_ADDS_rail.BBP_OT_add_straight_rail.bl_idname)
|
||||
sublayout.operator(OP_ADDS_rail.BBP_OT_add_transition_rail.bl_idname)
|
||||
sublayout.operator(OP_ADDS_rail.BBP_OT_add_side_rail.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.label(text="Curve Rails", icon='MOD_SCREW', text_ctxt='BBP/__init__.reuse_draw_add_rail()')
|
||||
sublayout = reuse_create_layout(layout, target)
|
||||
sublayout.operator(OP_ADDS_rail.BBP_OT_add_arc_rail.bl_idname)
|
||||
sublayout.operator(OP_ADDS_rail.BBP_OT_add_spiral_rail.bl_idname)
|
||||
sublayout.operator(OP_ADDS_rail.BBP_OT_add_side_spiral_rail.bl_idname)
|
||||
|
||||
def reuse_draw_add_component(layout: bpy.types.UILayout, target: DrawTarget):
|
||||
# We only use Grid for basic components
|
||||
layout.label(text="Basic Components", text_ctxt='BBP/__init__.reuse_draw_add_component()')
|
||||
OP_ADDS_component.BBP_OT_add_component.draw_blc_menu(reuse_create_layout(layout, target))
|
||||
|
||||
layout.separator()
|
||||
layout.label(text="Nong Components", text_ctxt='BBP/__init__.reuse_draw_add_component()')
|
||||
sublayout = reuse_create_layout(layout, target)
|
||||
OP_ADDS_component.BBP_OT_add_nong_extra_point.draw_blc_menu(sublayout)
|
||||
OP_ADDS_component.BBP_OT_add_nong_ventilator.draw_blc_menu(sublayout)
|
||||
|
||||
layout.separator()
|
||||
layout.label(text="Series Components", text_ctxt='BBP/__init__.reuse_draw_add_component()')
|
||||
sublayout = reuse_create_layout(layout, target)
|
||||
OP_ADDS_component.BBP_OT_add_tilting_block_series.draw_blc_menu(sublayout)
|
||||
OP_ADDS_component.BBP_OT_add_swing_series.draw_blc_menu(sublayout)
|
||||
OP_ADDS_component.BBP_OT_add_ventilator_series.draw_blc_menu(sublayout)
|
||||
|
||||
layout.separator()
|
||||
layout.label(text="Components Pair", text_ctxt='BBP/__init__.reuse_draw_add_component()')
|
||||
sublayout = reuse_create_layout(layout, target)
|
||||
OP_ADDS_component.BBP_OT_add_sector_component_pair.draw_blc_menu(sublayout)
|
||||
|
||||
class BBP_MT_AddBmeMenu(bpy.types.Menu):
|
||||
"""Add Ballance Floor"""
|
||||
@ -59,8 +97,7 @@ class BBP_MT_AddBmeMenu(bpy.types.Menu):
|
||||
bl_translation_context = 'BBP_MT_AddBmeMenu'
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
OP_ADDS_bme.BBP_OT_add_bme_struct.draw_blc_menu(layout)
|
||||
reuse_draw_add_bme(self.layout, DrawTarget.BldMenu)
|
||||
|
||||
class BBP_MT_AddRailMenu(bpy.types.Menu):
|
||||
"""Add Ballance Rail"""
|
||||
@ -69,54 +106,93 @@ class BBP_MT_AddRailMenu(bpy.types.Menu):
|
||||
bl_translation_context = 'BBP_MT_AddRailMenu'
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
reuse_draw_add_rail(self.layout, DrawTarget.BldMenu)
|
||||
|
||||
layout.label(text="Sections", icon='MESH_CIRCLE', text_ctxt='BBP_MT_AddRailMenu/draw')
|
||||
layout.operator(OP_ADDS_rail.BBP_OT_add_rail_section.bl_idname)
|
||||
layout.operator(OP_ADDS_rail.BBP_OT_add_transition_section.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.label(text="Straight Rails", icon='IPO_CONSTANT', text_ctxt='BBP_MT_AddRailMenu/draw')
|
||||
layout.operator(OP_ADDS_rail.BBP_OT_add_straight_rail.bl_idname)
|
||||
layout.operator(OP_ADDS_rail.BBP_OT_add_transition_rail.bl_idname)
|
||||
layout.operator(OP_ADDS_rail.BBP_OT_add_side_rail.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.label(text="Curve Rails", icon='MOD_SCREW', text_ctxt='BBP_MT_AddRailMenu/draw')
|
||||
layout.operator(OP_ADDS_rail.BBP_OT_add_arc_rail.bl_idname)
|
||||
layout.operator(OP_ADDS_rail.BBP_OT_add_spiral_rail.bl_idname)
|
||||
layout.operator(OP_ADDS_rail.BBP_OT_add_side_spiral_rail.bl_idname)
|
||||
|
||||
class BBP_MT_AddComponentsMenu(bpy.types.Menu):
|
||||
class BBP_MT_AddComponentMenu(bpy.types.Menu):
|
||||
"""Add Ballance Component"""
|
||||
bl_idname = "BBP_MT_AddComponentsMenu"
|
||||
bl_label = "Components"
|
||||
bl_translation_context = 'BBP_MT_AddComponentsMenu'
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
reuse_draw_add_component(self.layout, DrawTarget.BldMenu)
|
||||
|
||||
layout.label(text="Basic Components", text_ctxt='BBP_MT_AddComponentsMenu/draw')
|
||||
OP_ADDS_component.BBP_OT_add_component.draw_blc_menu(layout)
|
||||
|
||||
class BBP_PT_SidebarAddBmePanel(bpy.types.Panel):
|
||||
"""Add Ballance Floor"""
|
||||
bl_label = "Floors"
|
||||
bl_idname = "BBP_PT_SidebarAddBmePanel"
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_region_type = 'UI'
|
||||
bl_context = "objectmode"
|
||||
bl_category = 'Ballance'
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
bl_translation_context = 'BBP_PT_SidebarAddBmePanel'
|
||||
|
||||
def draw(self, context):
|
||||
reuse_draw_add_bme(self.layout, DrawTarget.BldPanel)
|
||||
|
||||
class BBP_PT_SidebarAddRailPanel(bpy.types.Panel):
|
||||
"""Add Ballance Rail"""
|
||||
bl_label = "Rails"
|
||||
bl_idname = "BBP_PT_SidebarAddRailPanel"
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_region_type = 'UI'
|
||||
bl_context = "objectmode"
|
||||
bl_category = 'Ballance'
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
bl_translation_context = 'BBP_PT_SidebarAddRailPanel'
|
||||
|
||||
def draw(self, context):
|
||||
reuse_draw_add_rail(self.layout, DrawTarget.BldPanel)
|
||||
|
||||
class BBP_PT_SidebarAddComponentPanel(bpy.types.Panel):
|
||||
"""Add Ballance Component"""
|
||||
bl_label = "Components"
|
||||
bl_idname = "BBP_PT_SidebarAddComponentPanel"
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_region_type = 'UI'
|
||||
bl_context = "objectmode"
|
||||
bl_category = 'Ballance'
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
bl_translation_context = 'BBP_PT_SidebarAddComponentPanel'
|
||||
|
||||
def draw(self, context):
|
||||
reuse_draw_add_component(self.layout, DrawTarget.BldPanel)
|
||||
|
||||
#endregion
|
||||
|
||||
#region Other Menu
|
||||
|
||||
class BBP_MT_View3DMenu(bpy.types.Menu):
|
||||
"""Ballance 3D related operators"""
|
||||
bl_idname = "BBP_MT_View3DMenu"
|
||||
bl_label = "Ballance"
|
||||
bl_translation_context = 'BBP_MT_View3DMenu'
|
||||
|
||||
def draw(self, context):
|
||||
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)
|
||||
layout.separator()
|
||||
layout.label(text="Nong Components", text_ctxt='BBP_MT_AddComponentsMenu/draw')
|
||||
OP_ADDS_component.BBP_OT_add_nong_extra_point.draw_blc_menu(layout)
|
||||
OP_ADDS_component.BBP_OT_add_nong_ventilator.draw_blc_menu(layout)
|
||||
|
||||
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="Series Components", text_ctxt='BBP_MT_AddComponentsMenu/draw')
|
||||
OP_ADDS_component.BBP_OT_add_tilting_block_series.draw_blc_menu(layout)
|
||||
OP_ADDS_component.BBP_OT_add_swing_series.draw_blc_menu(layout)
|
||||
OP_ADDS_component.BBP_OT_add_ventilator_series.draw_blc_menu(layout)
|
||||
|
||||
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="Components Pair", text_ctxt='BBP_MT_AddComponentsMenu/draw')
|
||||
OP_ADDS_component.BBP_OT_add_sector_component_pair.draw_blc_menu(layout)
|
||||
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()
|
||||
layout.label(text='Material', icon='MATERIAL', text_ctxt='BBP_MT_View3DMenu/draw')
|
||||
layout.operator(OP_MTL_fix_materials.BBP_OT_fix_all_materials.bl_idname)
|
||||
|
||||
# ===== Menu Drawer =====
|
||||
#endregion
|
||||
|
||||
MenuDrawer_t = typing.Callable[[typing.Any, typing.Any], None]
|
||||
#region Menu Drawer
|
||||
|
||||
TFctMenuDrawer = typing.Callable[[typing.Any, typing.Any], None]
|
||||
|
||||
def menu_drawer_import(self, context) -> None:
|
||||
layout: bpy.types.UILayout = self.layout
|
||||
@ -154,16 +230,17 @@ def menu_drawer_add(self, context) -> None:
|
||||
layout.label(text="Ballance", text_ctxt='BBP/__init__.menu_drawer_add()')
|
||||
layout.menu(BBP_MT_AddBmeMenu.bl_idname, icon='MESH_CUBE')
|
||||
layout.menu(BBP_MT_AddRailMenu.bl_idname, icon='MESH_CIRCLE')
|
||||
layout.menu(BBP_MT_AddComponentsMenu.bl_idname, icon='MESH_ICOSPHERE')
|
||||
layout.menu(BBP_MT_AddComponentMenu.bl_idname, icon='MESH_ICOSPHERE')
|
||||
|
||||
def menu_drawer_grouping(self, context) -> None:
|
||||
layout: bpy.types.UILayout = self.layout
|
||||
layout.separator()
|
||||
|
||||
# NOTE: because outline context may change operator context
|
||||
# YYC MARK:
|
||||
# Because outline context change operator context into EXEC_*,
|
||||
# so it will cause no popup window when click operator in outline.
|
||||
# thus we create a sub layout and set its operator context as 'INVOKE_DEFAULT'
|
||||
# thus, all operators can pop up normally.
|
||||
# Thus we create a sub layout and set its operator context as 'INVOKE_DEFAULT',
|
||||
# so that all operators can pop up normally.
|
||||
col = layout.column()
|
||||
col.operator_context = 'INVOKE_DEFAULT'
|
||||
|
||||
@ -188,7 +265,8 @@ def menu_drawer_naming_convention(self, context) -> None:
|
||||
layout: bpy.types.UILayout = self.layout
|
||||
layout.separator()
|
||||
|
||||
# same reason in `menu_drawer_grouping()``
|
||||
# YYC MARK:
|
||||
# Same reason for changing operator context introduced in `menu_drawer_grouping()`
|
||||
col = layout.column()
|
||||
col.operator_context = 'INVOKE_DEFAULT'
|
||||
|
||||
@ -199,37 +277,43 @@ def menu_drawer_naming_convention(self, context) -> None:
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
#region Register and Unregister.
|
||||
|
||||
g_BldClasses: tuple[typing.Any, ...] = (
|
||||
BBP_MT_View3DMenu,
|
||||
BBP_MT_AddBmeMenu,
|
||||
BBP_MT_AddRailMenu,
|
||||
BBP_MT_AddComponentsMenu
|
||||
BBP_MT_AddComponentMenu,
|
||||
|
||||
BBP_PT_SidebarAddBmePanel,
|
||||
BBP_PT_SidebarAddRailPanel,
|
||||
BBP_PT_SidebarAddComponentPanel,
|
||||
)
|
||||
|
||||
class MenuEntry():
|
||||
mContainerMenu: bpy.types.Menu
|
||||
mIsAppend: bool
|
||||
mMenuDrawer: MenuDrawer_t
|
||||
def __init__(self, cont: bpy.types.Menu, is_append: bool, menu_func: MenuDrawer_t):
|
||||
mMenuDrawer: TFctMenuDrawer
|
||||
def __init__(self, cont: bpy.types.Menu, is_append: bool, menu_func: TFctMenuDrawer):
|
||||
self.mContainerMenu = cont
|
||||
self.mIsAppend = is_append
|
||||
self.mMenuDrawer = menu_func
|
||||
|
||||
g_BldMenus: tuple[MenuEntry, ...] = (
|
||||
MenuEntry(bpy.types.VIEW3D_MT_editor_menus, False, menu_drawer_view3d),
|
||||
MenuEntry(bpy.types.TOPBAR_MT_file_import, True, menu_drawer_import),
|
||||
MenuEntry(bpy.types.TOPBAR_MT_file_export, True, menu_drawer_export),
|
||||
MenuEntry(bpy.types.VIEW3D_MT_add, True, menu_drawer_add),
|
||||
MenuEntry(bpy.types.VIEW3D_MT_editor_menus, False, menu_drawer_view3d),
|
||||
MenuEntry(bpy.types.TOPBAR_MT_file_import, True, menu_drawer_import),
|
||||
MenuEntry(bpy.types.TOPBAR_MT_file_export, True, menu_drawer_export),
|
||||
MenuEntry(bpy.types.VIEW3D_MT_add, True, menu_drawer_add),
|
||||
|
||||
MenuEntry(bpy.types.VIEW3D_MT_object_context_menu, True, menu_drawer_snoop_then_conv),
|
||||
MenuEntry(bpy.types.VIEW3D_MT_object_context_menu, True, menu_drawer_snoop_then_conv),
|
||||
|
||||
# register double (for 2 menus)
|
||||
MenuEntry(bpy.types.VIEW3D_MT_object_context_menu, True, menu_drawer_grouping),
|
||||
MenuEntry(bpy.types.OUTLINER_MT_object, True, menu_drawer_grouping),
|
||||
# Register this twice (for 2 menus respectively)
|
||||
MenuEntry(bpy.types.VIEW3D_MT_object_context_menu, True, menu_drawer_grouping),
|
||||
MenuEntry(bpy.types.OUTLINER_MT_object, True, menu_drawer_grouping),
|
||||
|
||||
MenuEntry(bpy.types.OUTLINER_MT_collection, True, menu_drawer_naming_convention),
|
||||
MenuEntry(bpy.types.OUTLINER_MT_collection, True, menu_drawer_naming_convention),
|
||||
)
|
||||
|
||||
def register() -> None:
|
||||
@ -266,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:
|
||||
@ -288,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()
|
||||
|
@ -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.
|
||||
|
@ -1,18 +1,25 @@
|
||||
# Add Floor
|
||||
|
||||
!!! info "Not latest version"
|
||||
This translated page is not the latest version because the modification of source page. Please see source page of the latest version.
|
||||
!!! info "BME is extensible"
|
||||
BME's floor adder is extensible, each item in the menu is actually described by a set of JSON data. You can read the [Technical Information](./tech-infos.md) section to learn how we write this JSON, and you can even expand the types of floors that BME can create to suit your needs.
|
||||
|
||||
## Start Generating
|
||||
|
||||
### From Menu
|
||||
|
||||
In the 3D view, click `Add - Floors` to expand the Add Floors menu. The menu is shown below.
|
||||
|
||||

|
||||
|
||||
Click on the menu to see all supported floor types in the submenu that pops up. Their names and icons hint at the style and shape of the floor it is intended to create.
|
||||
Click on the menu to see all supported floor types by categories in the submenu that pops up. Their names and icons hint at the style and shape of the floor it is intended to create.
|
||||
|
||||
!!! info "BME is extensible"
|
||||
BME's floor adder is extensible, each item in the menu is actually described by a set of JSON data. You can read the [Technical Information](./tech-infos.md) section to learn how we write this JSON, and you can even expand the types of floors that BME can create to suit your needs.
|
||||
### From Sidebar
|
||||
|
||||
Additionally, you can press `N` on keyboard to expand the sidebar of 3D view and find `Ballance` tab inside it. You also can find these adder by clicking it and expanding `Floor` panel as presented in following image:
|
||||
|
||||

|
||||
|
||||
Comparing with menu, the advantage of this solution is that the sidebar is persistent in UI layout. It is more convenient and effective when adding multiple floors by reducing the time of repetitive opening add menu. Also, there is Rail and Component adder panels under this tab. This will not be introduced again in following chapters.
|
||||
|
||||
## Configure Floor
|
||||
|
||||
@ -26,11 +33,14 @@ Then it also asks us to provide the height of the platform, which defaults to 5,
|
||||
|
||||
Finally, it tells us which sides of the floor we need to configure to display. Note that Top and Bottom are the top and bottom surfaces along the height direction (Z axis), while Front, Back, Left, and Right are the front, back, left, and right surfaces when looking down with your head on the -X axis and your eyes on the -Z axis. You may notice that there is a perspective cube in the center of these six face buttons, and in fact the positions of these six face options correspond to the positions of the six faces of this perspective cube.
|
||||
|
||||
## Extra Transform
|
||||
|
||||
At the bottom of the BME configuration dialog, you can always find an area called Extra Transform. In this area, there are two options for configuration: extra translation and rotation. These fields are primarily intended for visual edit.
|
||||
|
||||
Before explaining the actual functions of these fields, it is important to understand that the floor created by BME is always generated based on the current position of the 3D cursor. In other words, wherever the 3D cursor is located, the newly created surface will be positioned accordingly. This design primarily considers visual edit, allowing users to first move the 3D cursor to the desired location for adding a floor, and then add the corresponding BME structure. This enables users to preview results in real-time while adjusting parameters. Sometimes, the position of your 3D cursor may not be entirely accurate, or the final generated structure may require certain rotation to meet your expectations. In such cases, Extra Transform fields can be utilized to apply extra translation and rotation to the generated structure in relation to the 3D cursor, ensuring it is accurately positioned. In this way, the correct results can be previewed when adjusting the parameters.
|
||||
|
||||
## Tips
|
||||
|
||||
Each floor type has a different number of configuration entries, so for different floor types, you will need to follow the configuration hint text to understand what the corresponding configuration does. Some floor types may have a large number of configuration entries, while others may have no configuration entries at all.
|
||||
|
||||
The default values for the floor type configuration are set to the values that were most commonly used when the floor was created. The values are reset to the defaults each time the floor type is switched or recreated.
|
||||
|
||||
|
||||
|
||||
|
@ -1,8 +1,5 @@
|
||||
# Compile and Distribute Plugin
|
||||
|
||||
!!! info "Not latest version"
|
||||
This translated page is not the latest version because the modification of source page. Please see source page of the latest version.
|
||||
|
||||
This page will guide you in compiling the plugin as well as distributing it.
|
||||
|
||||
## Compiling LibCmo with BMap
|
||||
@ -15,11 +12,93 @@ Then we need to configure PyBMap, which comes with LibCmo. Please follow the man
|
||||
|
||||
Then we need to copy the configured PyBMap to our project under `bbp_ng/PyBMap` to complete this step.
|
||||
|
||||
## Generate Thumbnails and Compress JSON
|
||||
## Generate Resources
|
||||
|
||||
BBP comes with a built-in set of custom icons, as well as the JSON files needed by its component BME to describe the structure. By batch generating thumbnails and compressing JSON operations, the size of these parts can be reduced, making them suitable for loading in Blender and easier to distribute.
|
||||
BBP needs some resoures to run, and these resources need to be processed before using them.
|
||||
|
||||
Go to the `bbp_ng/tools` folder and run `python3 build_icons.py` which will batch generate thumbnails (this requires the PIL library, please install it via pip in advance). It actually generates thumbnails from the original images in the `bbp_ng/raw_icons` directory and stores them in the `bbp_ng/icons` folder. Running `python3 build_jsons.py` will compress the JSON, which actually reads, compresses, and writes the raw JSON files from the `bbp_ng/raw_jsons` directory into the `bbp_ng/jsons` folder.
|
||||
For generating these resrouces, we firstly need to navigate to `scripts` directory, and execute `uv sync` to restore the environment for scripts (Astral UV required).
|
||||
|
||||
### Generate Thumbnails
|
||||
|
||||
BBP comes with a built-in set of custom icons, however these icons are stored as their original size in repository for keeping convenient editing and high quality. We need to reduce the size of these icons to make them are easy to be loaded on Blender and easy for distribution by generating thumbnails for them.
|
||||
|
||||
Execute `uv run build_icons.py` to generate thumbnails. It actually generates thumbnails from the original images in the `assets/icons` directory and stores them in the `bbp_ng/icons` folder.
|
||||
|
||||
### Generate JSONs
|
||||
|
||||
The BME component in BBP relies on a series of JSON files to describe the prototype. These profiles are stored in the library in JSON5 format, making them easy for writers to read and write. We converte these JSON5 files to JSON files and compressing their size makes them easier to load in Blender, as well as to facilitate plugin distribution, by batchly generating them.
|
||||
|
||||
If you are the plugin developer or writer of these prototypes, you need to do an additional thing before generating these JSON files: verify these JSON files. The BBP plugin will assume that these JSON files are correct when loading them. If you put a JSON file with errors (e.g. missing some fields or has some typos, etc.), it will cause Blender to throw an error when creating prototype. Therefore, it is necessary to verify these JSON files. Execute `uv run validate_jsons.py` to verify all prototype files. If there are no errors, it means that everything is okey. It is important to note that the validator is not perfect, it can only verify the data as much as possible to ensure that some common erros (e.g. typo in field name) will not occur. It can not make 100% sure about that there is no error inside these files.
|
||||
|
||||
For compilers, all you need to do is that execute `uv run build_jsons.py` to generate JSON files. It actually reads, compresses, and writes the original JSON5 files in `assets/jsons` directory to the `bbp_ng/jsons` folder in JSON format.
|
||||
|
||||
### Generate Element Meshes
|
||||
|
||||
BBP has built-in mesh data for all Ballance element placeholders. Execute `uv run build_jsons.py` to deploy these meshes, which simply copies the mesh files under `assets/jsons` folder to `bbp_ng/jsons` folder.
|
||||
|
||||
## Translation
|
||||
|
||||
The BBP plugin supports multilingual functionality, so we need to extract and update the content to be translated before the official release, and then proceed to the next step after translating all the content.
|
||||
|
||||
Blender's multilingual support for plugins is not satisfactory, and BBP's design is relatively special, so BBP adopts a different way to manage translations than the official recommended plugin translation management method of Blender: that is, use PO files to manage translations instead of the officially recommended Python script format.
|
||||
|
||||
!!! info "Do NOT submit translations in Python format"
|
||||
As mentioned above, BBP uses PO files to manage translations, rather than the Python source format recommended by Blender. However, this does not prevent Blender's multilingual plugins from writing translations in the Python source format into the plugin's source code. Submitting duplicate translations not only increases the repository size but also complicates management. Therefore, BBP requires you to delete the Python-format translations before submission.
|
||||
|
||||
The specific operational method is to open the `bbp_ng/UTIL_translation.py` file before submission and change the value of the translation tuple variable `translations_tuple` to an empty tuple (i.e., `translations_tuple = ()`).
|
||||
|
||||
### Extract Translation Template
|
||||
|
||||
Before translating, it is important to first recognize that the text requiring translation for BBP consists of two parts. One part is the BBP plugin itself, whose text to be translated can be extracted by Blender's built-in multilingual plugin. The other part is the JSON file in the BME component that describes the structure, where the names of various showcase fields need to be translated. However, this part cannot be handled by Blender's multilingual plugin, as it is dynamically loaded. Fortunately, we have written an extractor that can extract the relevant text to be translated from the BME's JSON file. To do this, execute `uv run extract_jsons.py` in the folder where the previous script was run, and the script will extract the text to be translated and write it into the `i18n/bme.pot` file. Therefore, the next task is simply to extract the translation for the plugin portion.
|
||||
|
||||
First, you need to enable Blender's built-in multilingual plugin, "Manage UI translations". To enable it, you may also need to download the source code and translation repository corresponding to your version of Blender. For specific instructions, please refer to the [official Blender Documentation](https://developer.blender.org/docs/handbook/translating/translator_guide/). Once you have enabled the plugin and configured the appropriate related paths in the preferences, you can find the "I18n Update Translation" panel under the "Render" panel. You can then proceed to extract the translations by following these steps:
|
||||
|
||||
1. First, ensure that all Blender processes are closed; otherwise, the plugin will remain in a loading state, and modifications to the translated tuple variables will not take effect.
|
||||
1. Change the value of the translation tuple variable `translations_tuple` in the plugin to an empty tuple (refer to the earlier mentioned submission notes). Setting the translation tuple to empty can reset the translation status of the plugin, ensuring that subsequent text extraction operations are not affected by any existing translations.
|
||||
1. Open Blender, go to the "I18n Update Translation" panel, click "Deselect All" to uncheck all languages, and then only check the boxes next to the following languages (as BBP currently supports a limited number of languages):
|
||||
* Simplified Chinese (简体中文)
|
||||
1. Click the "Refresh I18n Data" button at the bottom of the section, then in the pop-up window, select "Ballance Blender Plugin". After a short wait, the plugin will complete the extraction of the characters to be translated. At this point, the plugin merely extracts the translation fields into the source code of the plugin in a format recommended by Blender, using Python source code.
|
||||
1. In order to obtain the desired editable POT file, you need to click the "Export PO" button. In the pop-up window, select the "Ballance Blender Plugin", and you can choose any folder for saving the location (for example, the desktop, as it will generate many files, including our desired POT file). Uncheck the "Update Existing" option on the right and ensure that "Export POT" is checked, then proceed to save. After the export is complete, you will find a translation template file named `blender.pot` and numerous `.po` files named after language identifiers in the folder you selected.
|
||||
1. You need to copy `blender.pot` to the `i18n` folder and rename it to `bbp_ng.pot`. At this point, we have extracted all the content that needs to be translated.
|
||||
|
||||
### Merge Translation Template
|
||||
|
||||
There are currently two POT files in the `i18n` folder, which represent two sets of extracted text awaiting translation. We need to merge them. Execute `xgettext -o blender.pot bbp_ng.pot bme.pot` in the `i18n` folder to perform the merge. The merged `i18n/blender.pot` will serve as the translation template.
|
||||
|
||||
### Create New Language Translation
|
||||
|
||||
If BBP needs to support more languages in the future, you will need to create the corresponding PO translation files for the new languages from the POT files. You can create them through one of the following methods.
|
||||
|
||||
* By using software such as Poedit to open the POT file, select function like create new translation from it, and then save to create.
|
||||
* Create a new language PO translation file using commands such as `msginit -i blender.pot -o zh_HANS.po -l zh_CN.utf8`.
|
||||
|
||||
There are various ways to create, but the only point to note is that you need to set the file name (if the file name is incorrect, Blender will refuse to accept it) and the area name (which will be used when using `msginit`, with the purpose of ensuring UTF8 format encoding) as shown in the table below.
|
||||
|
||||
|Language|File Name|Area Name|
|
||||
|:---|:---|:---|
|
||||
|Simplified Chinese (简体中文)|`zh_HANS.po`|`zh_CN.utf8`|
|
||||
|
||||
### Update Language Translation
|
||||
|
||||
Creating new language translations is not common; a more common practice is to update existing language translation files based on translation templates. You can update them through one of the following methods.
|
||||
|
||||
* Open the PO file using software such as Poedit, and then select to update from the POT file.
|
||||
* Update using commands such as `msgmerge -U zh-HANS.po blender.pot --backup=none`.
|
||||
|
||||
### Start Translating
|
||||
|
||||
After updating the PO translation files for all languages, you may choose your preferred method for translation, such as using Poedit or editing directly.
|
||||
|
||||
The BBP requires the use of the KDE community's translation standards to standardize the translations of plugins. For example, you can find the KDE community's translation standards for Simplified Chinese on the [KDE China](https://kde-china.org/tutorial.html) website.
|
||||
|
||||
### Write Translation Back
|
||||
|
||||
The translation in PO format cannot be recognized by Blender. Therefore, after the translation is completed, you also need to utilize Blender's multilingual plugin to convert the PO file back into a translation in Python source code format that Blender can recognize. Due to issues with the design of Blender's multilingual plugin, we cannot directly use the "Import PO" function to convert the PO file back into Python source code format. You need to follow the steps below sequentially in order to import the PO translation into the plugin:
|
||||
|
||||
1. First, ensure that all Blender processes are closed; otherwise, the plugin will remain in a loading state, and modifications to the translated tuple variables will not take effect.
|
||||
1. Change the value of the translation tuple variable `translations_tuple` in the plugin to an empty tuple (refer to the earlier notes regarding submissions). The purpose of this step is to ensure that the entire plugin lacks translation entries, so that when using the "Import PO" feature, Blender's multilingual plugin will consider all fields stored in the PO file as needing translation, thereby preventing situations where only a portion of the translations is imported (as the translations for the BME portion were merged later).
|
||||
1. Open Blender, navigate to the "I18n Update Translation" panel, and following the procedure used when extracting the translation template, select only the languages that need to be translated from the language list.
|
||||
1. Click the "Import PO" button in the bottom row, then select the "Ballance Blender Plugin" in the pop-up window, and choose the `i18n` folder for import. In this way, we have completed the process of importing the PO file into a Python source code format recognizable by Blender.
|
||||
|
||||
## Packaging
|
||||
|
||||
@ -29,13 +108,11 @@ Assuming that the final output file is `redist/bbp_ng.zip`. If you are in the ro
|
||||
|
||||
Blender will package the plugin according to the instructions in `blender_manifest.toml` with the following files excluded:
|
||||
|
||||
* `bbp_ng/raw_icons`: raw thumbnail folder.
|
||||
* `bbp_ng/raw_jsons`: raw JSON folder.
|
||||
* `bbp_ng/tools`: tools for compiling.
|
||||
* `bbp_ng/.style.yapf`: code style description file.
|
||||
* `bbp_ng/.gitignore`: gitignore
|
||||
* `bbp_ng/icons/.gitkeep`: folder placeholder
|
||||
* `bbp_ng/jsons/.gitkeep`: folder placeholder
|
||||
* `__pycache__/`:Python cache.
|
||||
* `.style.yapf`:code style description file.
|
||||
* `.gitignore`:gitignore
|
||||
* `.gitkeep`:folder placeholder
|
||||
* `.md`:documentation
|
||||
|
||||
## Generating Help Documentation
|
||||
|
||||
|
@ -19,7 +19,9 @@ The BBP plugin currently has 2 settings to configure.
|
||||
|
||||
Please fill in the `Texture` directory of Ballance, from which the plugin will use the external texture files (i.e. the ones Ballance originally came with). Click on the folder button on the right to browse the folders and select it.
|
||||
|
||||
This is crucial for BBP to work properly, and only if it is filled out correctly will BBP not make errors during operation.
|
||||
This option is almost mandatory. If you don't fill it, various core functions like Virtools file import export, BME creation, rail creation, etc. will not be available (button is in grey color).
|
||||
|
||||
This is crucial for BBP to work properly, and only if it is filled correctly will BBP not make errors during operation.
|
||||
|
||||
### No Component Collection
|
||||
|
||||
|
@ -1,8 +1,5 @@
|
||||
# Import and Export Virtools Document
|
||||
|
||||
!!! info "Not latest version"
|
||||
This translated page is not the latest version because the modification of source page. Please see source page of the latest version.
|
||||
|
||||
!!! warning "This is experimental content"
|
||||
Native importing and exporting of Virtools documents is experimental content for the BBP plugin, it may have many problems, see the [Report Issue](./report-bugs.md) section to learn more. When problems are encountered, please report them. the authors of the BBP plugin are not responsible for any consequences resulting from problems with the BBP plugin.
|
||||
|
||||
@ -12,7 +9,7 @@ Virtools files can be imported by clicking `File - Import - Virtools File`. Impo
|
||||
|
||||
### Conflict Options
|
||||
|
||||
The Conflict Options section indicates what to do when the importer encounters duplicate object names. There are 4 levels, for Object, Mesh, Material and Texture. There are 2 ways to handle it: Rename and Use Current. When Rename is selected and a duplicate name is encountered, a suffix will be added to the name to make it unique. By choosing Use Current, the import of the item from the file will be ignored and the item with the same name will be used instead, which already exists in the Blender document.
|
||||
The Conflict Options section indicates what to do when the importer encounters duplicate object names. There are 5 levels, for Object, Light, Mesh, Material and Texture. There are 2 ways to handle it: Rename and Use Current. When Rename is selected and a duplicate name is encountered, a suffix will be added to the name to make it unique. By choosing Use Current, the import of the item from the file will be ignored and the item with the same name will be used instead, which already exists in the Blender document.
|
||||
|
||||
!!! info "Differences from Virtools conflict resolution"
|
||||
Compared to the conflict resolution dialog in Virtools, the conflict resolution options provided by the BBP plugin do not support replacement, and the granularity is not fine-tuned to individual instances, but only for an entire type. So you can't set a different conflict resolution for each instance of a conflict individually. However, this setting is sufficient for most scenarios.
|
||||
@ -21,7 +18,7 @@ The default values for the options in the Conflict Options section are the solut
|
||||
|
||||
### Virtools Params
|
||||
|
||||
It is well known that Virtools uses a system-based multi-byte character encoding to process documents, and is therefore prone to what is known as garbling; Blender itself does not suffer from garbling, however, if we do not read a Virtools document with the correct encoding, the characters stored in it may still appear garbled when the Virtools document is imported into Blender. The Encodings property in the Virtools Params section specifies the encodings for reading Virtools documents. Multiple encodings can be specified, separated by a `;` (semicolon). Some common encodings are listed below:
|
||||
It is well known that Virtools uses a system-based multi-byte character encoding to process documents, and is therefore prone to what is known as garbling; Blender itself does not suffer from garbling, however, if we do not read a Virtools document with the correct encoding, the characters stored in it may still appear garbled when the Virtools document is imported into Blender. The Encodings property in the Virtools Params section specifies the encodings for reading Virtools documents. Multiple encodings may be specified, with the encodings higher up in the list being prioritized for use. The next level of encoding will only be utilized when decoding with the current level fails. Some common encodings are listed below:
|
||||
|
||||
* cp1252: Western European encoding used by Ballance.
|
||||
* gbk: The default encoding for Chinese Windows system.
|
||||
@ -42,7 +39,12 @@ Virtools files can be exported by clicking `File - Export - Virtools File`. Clic
|
||||
|
||||
### Export Target
|
||||
|
||||
The Export Target section is used to determine which objects you need to export to a Virtools document. You can choose to export a collection or an object and select the corresponding collection or object below. Note that selecting a collection will export the objects in the internal collection as well, i.e. exporting nested collections is supported.
|
||||
The Export Target section is used to determine which objects you need to export to a Virtools document. You can choose 1 of 4 options:
|
||||
|
||||
* Object: Export single object. Select an object in following input box.
|
||||
* Collection: Export single collection. **This is the most commonly used option.** Select a collection in following input box. Note that selecting a collection will export the objects in the internal collection as well, i.e. exporting nested collections is supported.
|
||||
* Selected Object: Export selected objects. Select the objects to be exported before enter this dialog.
|
||||
* All Objects: Export all objects inside this document. This option should be used with caution. Because it brutely iterate the list of document objects to export, and it is likely to export many objects you don't need.
|
||||
|
||||
### Virtools Params
|
||||
|
||||
@ -59,3 +61,11 @@ The Ballance Params section contains parameters that optimize the export process
|
||||
Successive Sector is an option to work around a bug that occurs when exporting groups of sectors. For some reason, if there are no elements in a sector (actually, no objects are grouped in a sector group), the export plugin thinks that the sector group doesn't exist and misses the export. And since Ballance determines the final sector, i.e. the sector where the spaceships appears, by incrementing the number of sectors from 1 to the last sector group that exists, the combination of the two causes Ballance to incorrectly determine the number of sectors in the map, and thus display the spaceships in the wrong sector, which is an export bug. when this option is checked, the exported document will be pre-defined according to the number of sectors specified in the Ballance Map information in the current Blender file. When this option is checked, the exported document will pre-create all of the sectors according to the number of sectors specified in the Ballance map information in the current Blender file before exporting, so that you don't miss creating some sector groups, and the spaceships will be displayed in the correct sectors.
|
||||
|
||||
This option is usually checked when exporting playable maps, if you just want to export some models then you need to turn this option off, otherwise it will create a lot of useless sector groups in the final file.
|
||||
|
||||
## Can't Import or Export
|
||||
|
||||
In general, after you have properly installed and configured the plugin according to the previously introduced steps, you should theoretically be able to use the import and export functions for Virtools documents. However, unexpected issues may arise. The inability to import or export refers to the buttons for importing and exporting Virtools documents being greyed out and unclickable. You will need to follow the steps outlined below for checking. If you are referring to an error occurring during the import and export process, please refer to the warning information at the top of this page.
|
||||
|
||||
First, check whether you have correctly configured the `External Texture Folder` setting of the plugin in the [Configure Plugin](configure-plugin.md) section. If you have not configured it, you will naturally be unable to import or export Virtools files. This is because importing and exporting Virtools files relies on the original Ballance texture data. Please configure this setting carefully according to the tutorial.
|
||||
|
||||
If you are certain that you have set the correct texture path, you may try clicking the menu `Window - Toggle System Console` to open the console. There may be some relevant error messages outputted there, such as a failure to load the underlying BMap library, etc. The failure to load the underlying BMap library typically only occurs on machines with rare architectures, such as Windows systems using Snapdragon processors, as the plugin we packaged does not include the Virtools file underlying reading library BMap that supports these platforms. In this situation, you have two options: one is to report this to the developers and wait for support, and the other is to compile the library on yourself (this is only recommended for those with extensive computer knowledge).
|
||||
|
@ -3,6 +3,16 @@
|
||||
!!! info "May be Outdated"
|
||||
This document is translated from other languages and may not always be up to date.
|
||||
|
||||
<!---
|
||||
!!! info "Not latest version"
|
||||
This translated page is not the latest version because the modification of source page. Please see source page of the latest version.
|
||||
--->
|
||||
|
||||
<!---
|
||||
!!! info "No Translation"
|
||||
This page has not been translated. Please see source page of this page.
|
||||
--->
|
||||
|
||||
Welcome to the Ballance Blender Plugin, the user manual for the free and open source Ballance map creation suite.
|
||||
|
||||
Ballance Blender Plugin (aka BBP) is a plugin that focuses on the creation of Ballance custom maps. It offers a wide range of features that can be used to create Ballance maps by anyone from novice to experienced mappers. BBP provides the ability to import and export formats used in Ballance maps, and a series of convenient features tailored to the creation of Ballance maps: for novice mappers, you can quickly assemble a map with prefabricated road blocks, and for the experienced mappers, the mapping tools needed to build complex structures are also provided.
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
## Determining the Version
|
||||
|
||||
The principle of BBP's Blender support is to support the latest **LTS** version, and to spend some time migrating the plugin after the latest LTS version is released. The current plugin version **4.0** is based on Blender version **4.2.x**.
|
||||
The principle of BBP's Blender support is to support the latest **LTS** version, and to spend some time migrating the plugin after the latest LTS version is released. The current plugin version **4.3** is based on Blender version **4.5.x**.
|
||||
|
||||
Theoretically, BBP will work fine on other versions of Blender if no major changes have been made. For example you can try to run BBP plugin based on Blender 3.6 LTS on Blender 4.0. However, the developers of BBP do not deal with bugs that only appear in non-LTS versions. before installing the plugin, please select the appropriate version.
|
||||
|
||||
|
@ -14,9 +14,11 @@ Legacy alignment supports aligning multiple objects to a single object by first
|
||||
|
||||
In the panel, `Align Axis` specifies the axis you want to align to, you can multi-select here to specify more than one axis, without specifying any axis you will not be able to do the alignment operation, and thus you will not be able to click the `Apply` button.
|
||||
|
||||
`Current Object` is the alignment reference object, which is the active object in the scene, usually the last object you selected. This option specifies what value you need to reference for alignment, with `Min` (minimum value on axis), `Center (Bounding Box)` (center of the bounding box), `Center (Axis)` (origin of the object), and `Max` (maximum value on axis) available. These options are consistent with the alignment options in 3ds Max.
|
||||
`Current Object` indicates which instance was picked as the alignment reference. You may choose an active object in the scene, typically the last object you selected, or the 3D cursor. It is important to note that if you select active object mode, the active object will be excluded from the alignment operation and will not be moved, as the reference object is immovable. Conversely, if you select the 3D cursor, the active object will be included within the scope of the alignment operation.
|
||||
|
||||
The `Target Objects` are the objects that are being aligned, there may be many of them, in this option it is also specified what values you need to refer to them for alignment. The options have the same meaning as `Current Object`.
|
||||
`Current Object Align Mode` is the alignment mode for aligning to a reference object, which only appears when you select the active object in `Current Object`. Since the 3D cursor is merely a point, while objects occupy a certain space, we need to select a point in this space according to a specific pattern (described later) to be used for subsequent alignment operations. In this option, you specify what value you need to align to, with available selections including `Min` (minimum value on axis), `Center (Bounding Box)` (center of the bounding box), `Center (Axis)` (origin of the object), and `Max` (maximum value on axis). These options are consistent with the alignment options in 3ds Max.
|
||||
|
||||
The `Target Objects Align Mode` are the objects that are being aligned, there may be many of them, in this option it is also specified what values you need to refer to them for alignment. The options have the same meaning as `Current Object`.
|
||||
|
||||
The `Apply` button, when clicked, will press the current page's configuration into the operation stack and reset the settings above, allowing you to start a new round of alignment operations without having to perform a legacy alignment again. The number of operations in the stack is shown below the `Apply` button.
|
||||
|
||||
|
@ -1,8 +1,5 @@
|
||||
# Add Rail
|
||||
|
||||
!!! info "Not latest version"
|
||||
This translated page is not the latest version because the modification of source page. Please see source page of the latest version.
|
||||
|
||||
In the 3D view, click `Add - Rails` to expand the Add Rails menu. The menu is shown on the left side of the image below.
|
||||
|
||||

|
||||
@ -63,6 +60,8 @@ The first thing you need to do with an arc rail is to specify the Angle and Radi
|
||||
|
||||
Steps of the arc rail, the number of steps indicates the number of segments of the arc rail, the larger the number, the smoother the arc rail looks, relatively, the vertices will be more, the storage space and rendering requirements are also higher, so you need to choose a reasonable value.
|
||||
|
||||
The Flip option for arc rail allows you to flip (in other word, mirror) the generated structure along a specified axis, and the flip options available on all three axes can accommodate the generation of all possible arc rail structures.
|
||||
|
||||
Arc rails also support double-rail mono-rail selection, you can create mono-rail arc rails and double-rail arc rails. The capping attribute is also supported.
|
||||
|
||||
### Spiral Rail
|
||||
@ -73,7 +72,7 @@ Spiral rails have an Iterations property, which indicates how many times the rai
|
||||
|
||||
The spiral rail also needs to set the Steps property, which has the same meaning as the arc rail. However, it should be noted that the number of steps refers to the number of steps in each iteration, not the overall number of steps. Therefore, when adjusting the iteration attribute, you do not need to change the Steps attribute again.
|
||||
|
||||
Side Spiral Rail also has a capping property.
|
||||
Side Spiral Rail also has capping and flip property.
|
||||
|
||||
### Side Spiral Rail
|
||||
|
||||
@ -82,3 +81,9 @@ Side Spiral Rail, similar to spiral rail, but the ball is rolled along the side,
|
||||
Side Spiral Rail does not have a Screw property, because Side Spiral Rail is designed so that adjacent spins share a common edge, so the screw is fixed.
|
||||
|
||||
The Radius, Iterations and Steps attributes in the Side Spiral Rail settings have the same meaning as the spiral rail. Side Spiral Rail also have a capping attribute.
|
||||
|
||||
Spiral Rail also has capping and flip property.
|
||||
|
||||
## Extra Transform
|
||||
|
||||
When adding straight rails and curved rails, you can always set a property known as the Extra Transform. Its effect is consistent with the Extra Transform field in adding floors, both serving the purpose of visual edit.
|
||||
|
@ -1,8 +1,5 @@
|
||||
# Report Issue
|
||||
|
||||
!!! info "Not latest version"
|
||||
This translated page is not the latest version because the modification of source page. Please see source page of the latest version.
|
||||
|
||||
## What Can Go Wrong
|
||||
|
||||
BBP is not perfect, and since BBP's Virtools file import/export module is written in C++, BBP is more prone to errors than other plugins, and the consequences of errors can be more serious (including but not limited to memory leaks, accidental deletion of user files, etc.).
|
||||
@ -15,7 +12,7 @@ In Blender, you will observe if the plugin execution goes wrong:
|
||||
|
||||
## What Part Went Wrong
|
||||
|
||||
For the BBP plugin, if you observe something like `BMap operation failed` in the Python exception output, or the `IronPad.log` file in the `<Plugin-Install-Location>/PyBMap` folder, it means that The BBP plugin's BMap section, written in C++, is in error, and **you need to immediately save your current Blender document and exit Blender**. Because the plugin is in an abnormal state at this point, you should not continue any operations.
|
||||
For the BBP plugin, if you observe something like `BMap operation failed` in the Python exception output, or the `blender.exe.<xxx>.log` file (`<xxx>` is a string of numbers) in the `%LOCALAPPDATA%/CrashDumps` folder (`%LOCALAPPDATA%` is a Windows environment variable. If you are not familiar with its purpose, you can simply copy and paste this address directly into the Windows Explorer, which will automatically navigate you to the correct location), it means that The BBP plugin's BMap section, written in C++, is in error, and **you need to immediately save your current Blender document and exit Blender**. Because the plugin is in an abnormal state at this point, you should not continue any operations.
|
||||
|
||||
If there is no such thing as the above, then this is just a normal Python code execution error and you don't need to worry too much about it, but the error is still fatal and it is recommended to exit Blender and report the error after doing all the necessary operations.
|
||||
|
||||
@ -29,4 +26,4 @@ If you can't do that and you have proper way to contact with plugin author, then
|
||||
|
||||
First of all you need to describe in detail how you raise this error and what are the results of this error. If you can upload the documentation that led to the error, please try to do so (if it's not convenient to post it publicly, you can send it to the author through a private way such as email).
|
||||
|
||||
You also need to provide the Python stack report output in the Blender console (use `Window - Toggle System Console` to open the console). If your error is an error in the BMap section, you also need to provide the `IronPad.log` and `IronPad.dmp` files in the `<Plugin-Install-Location>/PyBMap` folder to make it easier for developers to locate the error.
|
||||
You also need to provide the Python stack report output in the Blender console (use `Window - Toggle System Console` to open the console). If your error is an error in the BMap section, you also need to provide the error log file `blender.exe.<xxx>.log` (where `<xxx>` represents a string of numbers) and the memory dump file `blender.exe.<xxx>.dmp` generated by the BMap module. These files can also be found in the `%LOCALAPPDATA%/CrashDumps` folder.
|
||||
|
@ -1,12 +1,30 @@
|
||||
# Technical Information
|
||||
|
||||
## Standards and Protocol Documentation
|
||||
|
||||
* BM File Specification: https://github.com/yyc12345/gist/blob/master/BMFileSpec/BMSpec_ZH.md
|
||||
* Mapping toolchain standards and format of files in the `meshes' folder: https://github.com/yyc12345/gist/blob/master/BMFileSpec/YYCToolsChainSpec_ZH.md
|
||||
* Format of the JSON file for BMERevenge: https://github.com/yyc12345/gist/blob/master/BMERevenge/DevDocument_v2.0_ZH.md
|
||||
|
||||
## Development Auxiliary Package
|
||||
|
||||
This plugin works with the `fake-bpy-module` module to implement type hinting to speed up development. Use the following command to install Blender's type hinting library.
|
||||
|
||||
* Blender 3.6: `pip install fake-bpy-module-latest==20230627`
|
||||
* Blender 4.2: `pip install fake-bpy-module-latest==20240716`
|
||||
* Blender 4.5: `pip install fake-bpy-module-latest==20250604`
|
||||
|
||||
The main reason for doing this is that `fake-bpy-module` doesn't release an official package for the given Blender version, so I had to install it by choosing the daily build closest to the release time of the corresponding Blender version.
|
||||
The primary reason for this is that the `fake-bpy-module` has not timely released packages suitable for the specified Blender version. Therefore, I can only install it by selecting the daily build version that is closest to the date of the corresponding Blender version leaving from the `main` branch (as daily builds only compile from the `main` branch).
|
||||
|
||||
!!! question "Why not use Blender's official bpy module?"
|
||||
Blender provides an official package named `bpy` on PyPI, but we will not adopt it as our development auxiliary package. This is because it basically repackages Blender into a module (which basically means you are downloading Blender again), allowing you to manipulate Blender through Python. This is contrary to our purpose of using a package that only provides type hints to assist in plugin development.
|
||||
|
||||
## Version Rule
|
||||
|
||||
The version number format of BBP follows [Semantic Versioning](https://semver.org), but with slight differences:
|
||||
|
||||
* The major version number is only increased when the entire plugin is reconstructed.
|
||||
* The minor version number is used for regular updates.
|
||||
* The patch version number is incremented when there is no modification for any functionalities. For example, version 4.2.1 only includes updates for macOS Blender without changing any features.
|
||||
|
||||
Before the formal release of BBP, there are typically three phased versions: Alpha version, Beta version, and RC version. The Alpha version focuses on functional updates, used to verify whether newly added or modified features are functioning correctly, and does not include documentation or translations. The Beta version is focus on plugin documentation, while the RC version concentrates on plugin translations. However, these three versions do not always exist; if the update content is minimal, some versions may be skipped, or a direct release may occur.
|
||||
|
@ -1,8 +1,5 @@
|
||||
# Virtools Properties
|
||||
|
||||
!!! info "Not latest version"
|
||||
This translated page is not the latest version because the modification of source page. Please see source page of the latest version.
|
||||
|
||||
## Virtools Group
|
||||
|
||||
The BBP plugin adds a new property to every Blender object, called Virtools Group. has the same functionality as Group in Virtools. Select an object and the `Virtools Group` panel can be found in the `Object` properties panel.
|
||||
@ -13,6 +10,8 @@ In the `Virtools Group` panel, you can click Add to group objects. After clickin
|
||||
|
||||
BBP also provides access to Virtools groups in Blender's other menus, see [Group Operation](./group-operations.md).
|
||||
|
||||
It is important to note that the Virtools group only works on mesh objects. When you open the Virtools group panel on other objects, you will see a warning message in the panel indicating that the Virtools group is invalid for that object. Although the Virtools group data set on non-mesh objects will be recognized and stored by Blender, it will not be saved to the Virtools file when exporting.
|
||||
|
||||
## Virtools Material
|
||||
|
||||
The plugin adds a new property to every Blender material, called Virtools Material, which bridges the gap between Virtools materials and Blender materials. Go to the `Material` properties panel and select a material to find the `Virtools Material` panel.
|
||||
@ -59,4 +58,12 @@ The BBP plugin adds a new property to all Blender meshes called Virtools Mesh. g
|
||||
|
||||

|
||||
|
||||
The Virtools Mesh is currently only used as a compatibility feature. It has only one property, Lit Mode, that can be set. Most early maps had black floor issue because they didn't know how to set the material correctly, so the Lit Mode was often set to Prelit to get the floor to show up properly. This attribute exists for compatibility with this compromise and the user usually does not need to set this option.
|
||||
The Virtools Mesh is currently only used as a compatibility feature. It has only one property, Lit Mode, that can be set. Most early maps had black floor issue because they didn't know how to set the material correctly, so the Lit Mode was often set to Prelit to get the floor to show up properly. This attribute exists for compatibility with this compromise and the user usually does not need to set this option.
|
||||
|
||||
## Virtools Light
|
||||
|
||||
The BBP plugin adds a new property called Virtools Light to all Blender lights. You can find the Virtools Light panel by navigating to the Data properties panel.
|
||||
|
||||

|
||||
|
||||
Similar to the materials in Virtools, the lighting system in Virtools differs significantly from that in Blender. The Virtools Light acts as a bridge, accurately reflecting the settings of Virtools, allowing for seamless storage within Blender files and providing necessary data during import and export. Additionally, this panel includes an Apply button to apply the Virtools lighting settings to Blender's lighting.
|
||||
|
@ -1,4 +1,37 @@
|
||||
# ZZQ Features
|
||||
|
||||
!!! info "No Translation"
|
||||
This page has not been translated. Please see source page of this page.
|
||||
This plugin received many suggestions from ZZQ during its development process, resulting in a rather disordered set of features, which are therefore described together on a separate page.
|
||||
|
||||
## Snoop Group then to Mesh
|
||||
|
||||
Snoop Group then to Mesh, fully referred to as: snoop and copy the Virtools Group infomations into corresponding curve object and convert it into mesh object. You can select certain objects and then right-click to find this function in the context menu of the object.
|
||||
|
||||
This function, as its name, converts the selected object into a mesh. If the selected object is a curve and there is a beveled object set, the grouping information of the beveled object will be assigned to the current curve (overriding the curve's current grouping settings). If the selected object is not a curve, or if it is a curve but lacks a beveled object, then this function is basically the same as executing a conversion to mesh. This function is extremely useful in loft modeling, as it only requires the correct grouping of the section object, after which using this function to convert the curve to a mesh will ensure that the grouping of the lofted object is correct.
|
||||
|
||||
## Game Camera
|
||||
|
||||
Many map makers often lack a clear concept of the size of their maps when creating them, which can easily lead to the production of maps that are either too large or too small. The in-game camera function of the menu item `Ballance - Game Camera` offers a way to preview the map from the perspective of the in-game camera within Blender, allowing map makers to better grasp the dimensions of the map.
|
||||
|
||||
In order to use this feature, there must first be a camera in your scene, set as the active camera of the scene (when no camera is present, a newly added camera through the add menu will be automatically set). Additionally, you will need an active object, which cannot be the same as this camera.
|
||||
|
||||
!!! question "Why is it necessary to have active object?"
|
||||
Due to the limitations of the Blender plugin, an active object is required since the in-game camera functionality supports targeting either the 3D cursor **or an active object**.
|
||||
|
||||
Typically, the situation where there are no active objects available only occurs in a blank Blender document, which consequently renders the functionality unavailable. In a custom map, there is no any lack of objects which can become active objects; simply clicking on any object is enough (when wishing to use the 3D cursor as a target).
|
||||
|
||||
After clicking this function, the view will automatically switch to the perspective of the active camera, facilitating your preview. You can adjust the relevant settings in the panel located at the bottom left.
|
||||
|
||||

|
||||
|
||||
First, select the target, which essentially means choosing the position of the player's ball. If 3D cursor is selected, it is treated as the origin of player's ball. If active object is selected, the player's ball is placed at the origin of the active object.
|
||||
|
||||
!!! info "The position of the player's ball is not that straightforward."
|
||||
Regardless of whether one chooses 3D cursor or active object, if no extremely fine adjustments are made, the preview results may have slight differences compared with in-game result.
|
||||
|
||||
When you use the 3D cursor mode and simply snap it to the ground, it does not represent the position of player ball. Since the radius of player ball is 2, you actually need to move 2 units in +Z direction to achieve the exact same perspective as in the game. However, this operation is too complex, and not performing this action does not cause a significant difference in the preview effect.
|
||||
|
||||
When using active object mode, it is important to note that the origin of the object corresponds to the position of the dot displayed in viewport when the object is selected. For most objects, this may not be what you desire, as they may be located inside or outside the object, and are not always positioned on the surface of the object or at the center of a particular face. To address this situation, I recommend using the 3D cursor as a target and, by entering edit mode, utilizing snapping and the `Shift + S` menu to place the cursor in the correct position.
|
||||
|
||||
Next is the selection of the camera's rotation angles. We offer 8 common used in-game preset angles, corresponding to 4 each for 90 degrees camera and 45 degrees camera. Furthermore, if these preset angles do not meet your requirements, you can also set custom angles.
|
||||
|
||||
Finally, there is the selection of camera perspectives, which can be divided into 3 types: Ordinary (standard perspective), Lift (perspective activated by holding the spacebar), and Easter Egg (special Easter Egg perspective).
|
||||
|
BIN
docs/docs/imgs/bme-adder-sidebar.png
Normal file
After Width: | Height: | Size: 108 KiB |
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 108 KiB |
BIN
docs/docs/imgs/game-camera.png
Normal file
After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 80 KiB |
@ -1,15 +1,25 @@
|
||||
# 添加路面
|
||||
|
||||
!!! info "BME是可扩展的"
|
||||
BME的路面添加器是可扩展的,菜单中的每一个项实际上都由一组JSON数据描述。您可以阅读[技术信息](./tech-infos.md)章节来了解我们是如何编写这些JSON的,甚至您还可以根据你的需求自行扩展BME可创建的路面种类。
|
||||
|
||||
## 开始生成
|
||||
|
||||
### 从添加菜单生成
|
||||
|
||||
在3D视图中,点击`Add - Floors`可展开添加路面菜单。菜单如下图所示。
|
||||
|
||||

|
||||
|
||||
点击菜单后可以在弹出的子菜单中查看所有受支持的路面类型。其名称和图标提示了它所要创建路面的样式与形状。
|
||||
点击菜单后可以在弹出的子菜单中按分类查看所有受支持的路面类型。其名称和图标提示了它所要创建路面的样式与形状。
|
||||
|
||||
!!! info "BME是可扩展的"
|
||||
BME的路面添加器是可扩展的,菜单中的每一个项实际上都由一组JSON数据描述。您可以阅读[技术信息](./tech-infos.md)章节来了解我们是如何编写这些JSON的,甚至您还可以根据你的需求自行扩展BME可创建的路面种类。
|
||||
### 从侧边栏生成
|
||||
|
||||
此外,还可以通过点按N键,打开3D视图的侧边栏,在其中找到Ballance选项卡,展开Floor面板也可找到,如下图所示:
|
||||
|
||||

|
||||
|
||||
该面板相较于添加菜单,其好处在于可常驻在界面之中,避免了在持续添加路面的操作中,频繁打开菜单寻找路面的麻烦。此外,该选项卡中还有用于添加钢轨和机关的面板可供展开,在后续章节中不再赘述。
|
||||
|
||||
## 配置路面
|
||||
|
||||
@ -27,7 +37,7 @@
|
||||
|
||||
在BME配置对话框的底部,您总是可以找到一个被称为额外变换的区域。在这个区域中,有额外移动和额外旋转两个选项可以配置。这些字段主要是为了可视化服务的。
|
||||
|
||||
在说明这些字段的实际作用前,你需要知道,BME创建的路面总是以当前3D游标的位置进行创建的,即3D游标在哪里,新创建的路面就在哪里。这主要是为了可视化来考虑到,用户可以先将3D游标移动到想要添加路面的位置,再添加对应的BME结构,便可以在调整参数的时候即时预览到结果。有时候你的3D游标的位置不是那么的准确,又或者最终生成的结构需要一定的旋转才是你期望的,此时就可以利用额外变换字段,为最终生成的结构,增加相对于3D游标的额外的移动和旋转,使之处于正确的位置,这样就可以在调整参数的时候实时预览到结果了。
|
||||
在说明这些字段的实际作用前,你需要知道,BME创建的路面总是以当前3D游标的位置进行创建的,即3D游标在哪里,新创建的路面就在哪里。这主要是为了可视化来考虑到,用户可以先将3D游标移动到想要添加路面的位置,再添加对应的BME结构,便可以在调整参数的时候即时预览到结果。有时候你的3D游标的位置不是那么的准确,又或者最终生成的结构需要一定的旋转才是你期望的,此时就可以利用额外变换字段,为最终生成的结构,增加相对于3D游标的额外的移动和旋转,使之处于正确的位置,这样就可以在调整参数的时候预览到正确的结果了。
|
||||
|
||||
## 小贴士
|
||||
|
||||
|
@ -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,18 +59,18 @@ 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`将用作翻译模板。
|
||||
|
||||
### 创建新语言翻译
|
||||
|
||||
如果未来BBP需要支持更多语言,你需要从POT文件为新语言创建其对应的PO翻译文件。你可以通过以下方式之一来创建它们。
|
||||
|
||||
1. 通过使用Poedit等软件打开POT文件,选择创建新的翻译,再进行保存来创建。
|
||||
1. 通过诸如`msginit -i blender.pot -o zh_HANS.po -l zh_CN.utf8`的命令来创建新语言的PO翻译文件。
|
||||
* 通过使用Poedit等软件打开POT文件,选择创建新的翻译,再进行保存来创建。
|
||||
* 通过诸如`msginit -i blender.pot -o zh_HANS.po -l zh_CN.utf8`的命令来创建新语言的PO翻译文件。
|
||||
|
||||
创建的方式多种多样,唯一需要注意的是你需要按下表所示设定文件名(文件名错误,Blender会拒绝接受)和区域名称(使用`msginit`时会用到,目的是确保是UTF8格式编码的)。
|
||||
|
||||
@ -64,8 +82,8 @@ Blender对于插件的多语言支持不尽如人意,且BBP的设计比较特
|
||||
|
||||
创建新的语言翻译并不常见,更为常见的操作是根据翻译模板,对现有语言翻译文件进行更新。你可以通过以下方式之一来更新它们
|
||||
|
||||
1. 通过Poedit等软件打开PO文件,再选择从POT文件更新。
|
||||
1. 通过诸如`msgmerge -U zh-HANS.po blender.pot --backup=none`的命令来更新。
|
||||
* 通过Poedit等软件打开PO文件,再选择从POT文件更新。
|
||||
* 通过诸如`msgmerge -U zh-HANS.po blender.pot --backup=none`的命令来更新。
|
||||
|
||||
### 进行翻译
|
||||
|
||||
@ -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源码格式的操作。
|
||||
|
||||
## 打包
|
||||
|
||||
@ -90,14 +108,11 @@ PO格式的翻译并不能被Blender识别,因此在翻译完成后,你还
|
||||
|
||||
Blender会根据`blender_manifest.toml`的指示,在排除下列文件的情况下将插件打包:
|
||||
|
||||
* `bbp_ng/i18n`:翻译文件夹。
|
||||
* `bbp_ng/raw_icons`:原始图片文件夹。
|
||||
* `bbp_ng/raw_jsons`:原始JSON文件夹。
|
||||
* `bbp_ng/tools`:编译用工具。
|
||||
* `bbp_ng/.style.yapf`:代码风格描述文件
|
||||
* `bbp_ng/.gitignore`:gitignore
|
||||
* `bbp_ng/icons/.gitkeep`:文件夹占位符
|
||||
* `bbp_ng/jsons/.gitkeep`:文件夹占位符
|
||||
* `__pycache__/`:Python缓存
|
||||
* `.style.yapf`:代码风格描述文件
|
||||
* `.gitignore`:gitignore
|
||||
* `.gitkeep`:文件夹占位符
|
||||
* `.md`:文档
|
||||
|
||||
## 生成帮助文档
|
||||
|
||||
|
@ -19,7 +19,9 @@ BBP插件目前有2个设置需要配置。
|
||||
|
||||
请填写为Ballance的`Texture`目录,插件将从此目录下调用外置贴图文件(即Ballance原本带有的贴图文件)。点击右侧的文件夹按钮可以浏览文件夹并选择。
|
||||
|
||||
这是关乎BBP是否正常运行的关键,只有填写正确,BBP才不会在运行中出错。
|
||||
该选项几乎是必填写的。如果不填写该项目,则Virtools文件导入导出,BME创建,钢轨创建等各种核心功能将不可用(按钮为灰色)。
|
||||
|
||||
该选项是BBP能否正常运行的关键,只有填写正确,BBP才不会在运行中出错。
|
||||
|
||||
### No Component Collection
|
||||
|
||||
|
@ -9,7 +9,7 @@
|
||||
|
||||
### 冲突解决选项
|
||||
|
||||
Conflict Options(冲突解决选项)章节指示了当导入器遇到物体名称重复的情况时,该如何处理。分为4个等级,分别针对Object(物体),Light(灯光),Mesh(网格),Material(材质),Texture(贴图)。处理方式则有2种:Rename(重命名)和Use Current(使用当前)。选择重命名后,当遇到重复名称时,将会自动为其添加名称后缀使其名称唯一化。而选择使用当前,则会忽略从文件中导入此项,转而使用Blender文档中已经存在的同名的项目。
|
||||
Conflict Options(冲突解决选项)章节指示了当导入器遇到物体名称重复的情况时,该如何处理。分为5个等级,分别针对Object(物体),Light(灯光),Mesh(网格),Material(材质),Texture(贴图)。处理方式则有2种:Rename(重命名)和Use Current(使用当前)。选择重命名后,当遇到重复名称时,将会自动为其添加名称后缀使其名称唯一化。而选择使用当前,则会忽略从文件中导入此项,转而使用Blender文档中已经存在的同名的项目。
|
||||
|
||||
!!! info "与Virtools冲突解决的不同"
|
||||
相比较于在Virtools的冲突解决对话框,BBP插件提供的冲突解决选项不支持替换功能,同时其粒度也不支持精细到单个实例,只能针对一整个类型进行设定。因此你无法单独为每一个冲突的实例设置不同的冲突解决方案。但目前这种设置已经能满足绝大对数的使用场景了。
|
||||
@ -39,7 +39,12 @@ Conflict Options(冲突解决选项)章节指示了当导入器遇到物体
|
||||
|
||||
### 导出目标
|
||||
|
||||
Export Target(导出目标)章节用于决定你需要将哪写物体导出到Virtools文档中。你可以选择导出一个集合或一个物体,并在下面选择对应的集合或物体。需要注意的是,选择集合的时候,会将内部集合中的物体也一起导出,即支持嵌套集合的导出。
|
||||
Export Target(导出目标)章节用于决定你需要将哪写物体导出到Virtools文档中。你可以在四种模式中选择其一,来决定需要导出的内容:
|
||||
|
||||
* Object:导出单个物体。选择后需要在下面选择需要导出的物体。
|
||||
* Collection:导出单个集合,**这是最常用的选项**。选择后需要在下面选择需要导出的集合。值得注意的是,该选项支持嵌套集合导出,即选择集合的时候,会将集合中的集合的物体也一起导出。
|
||||
* Selected Objects:导出选择的物体。你需要在导出前选择好需要导出的物体。
|
||||
* All Objects:导出该文档中的所有物体。该选项慎用,因为它是粗暴地遍历文档中的物体列表来进行导出,很可能会导出许多你不需要的物体。
|
||||
|
||||
### Virtools参数
|
||||
|
||||
@ -56,3 +61,11 @@ Ballance Params(Ballance参数)章节包含针对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。在面对这种情况时,你有两个选择,一是汇报给开发者,等待开发者主动支持,二是自行编译该库(仅建议有丰富计算机知识的人这样做)。
|
||||
|
@ -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`按钮下方显示。
|
||||
|
||||
|
@ -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版本则关注于插件翻译。但这三个版本并非总是存在,如果更新内容较少,则可能会跳过其中一些版本,或直接进行发布。
|
||||
|
@ -7,3 +7,31 @@
|
||||
窥视归组并转换为网格,其全称为:窥视并复制曲线倒角物体的Virtools归组信息后再转换为网格。你可以选中一些物体后右键,在物体上下文菜单中找到这一功能。
|
||||
|
||||
该功能正如其名,其将选中的物体转换为网格,如果选中的物体是曲线,且设置了倒角物体,则将倒角物体的归组信息赋予当前曲线(覆盖曲线当前归组设置)。如果选中的物体不是曲线,或者是曲线但没有倒角物体,那么该功能与执行转换为网格无异。该功能在放样建模时极为有用,因为只需要为截面物体进行正确的归组,然后再使用此功能将曲线转换为网格,就可以确保放样后的物体归组正确。
|
||||
|
||||
## 游戏内摄像机
|
||||
|
||||
许多制图者在制作地图时,往往对地图大小没有概念,很容易创建出过大或过小的地图。菜单项`Ballance - Game Camera`的游戏内摄像机功能则提供了一种在Blender内以游戏内摄像机视角预览地图的功能,方便制图者可以把握地图的大小。
|
||||
|
||||
为了使用该功能,你的场景中必须首先有一个摄像机,且被设置为场景的活动摄像机(无摄像机时,通过添加菜单新添加的摄像机会被自动设置)。然后你还需要一个活动物体,且该活动物体不能是这个摄像机。
|
||||
|
||||
!!! question "为什么一定需要活动物体?"
|
||||
由于Blender插件的限制,活动物体是必须的,因为游戏内摄像机功能支持以3D游标 **或活动物体** 为目标。
|
||||
|
||||
通常只有在一个空白的Blender文档中,才会出现无活动物体可用的情况,进而导致该功能不可用。对于一张自制地图,最不缺少的就是可成为活动物体的物体,只需要随便点击一个物体就可以得到(当想使用3D游标作为目标时)。
|
||||
|
||||
点击该功能后,视图将自动转为活动摄像机的视角,方便你进行预览。你可以在左下角的面板中调整相关设置。
|
||||
|
||||

|
||||
|
||||
首先是选择目标,实际上就是选择玩家球位于哪里。如果选择3D游标,则将3D游标视为玩家球。如果选择活动物体,则将玩家球放置在活动物体的原点。
|
||||
|
||||
!!! info "玩家球的位置并非那么简单"
|
||||
无论是选择3D游标,还是选择活动物体,如果不做特别精细的调整,其预览的结果和游戏中会有细微出入。
|
||||
|
||||
当你使用3D游标模式并将其简单地吸附到地面上时,它并非代表球的位置。因为玩家球是一个半径为2的球,实际上你需要向+Z方向移动2个单位长度才能够得到和游戏中一模一样的视角。但这个操作过于繁琐,不执行这个操作,预览的效果也不会偏差太多。
|
||||
|
||||
当你使用活动物体模式时,需要注意物体的原点是选择物体时显示的那个圆点的位置。对于大多数物体,这可能并非你想要的,因为他们可能位于物体内部或外部,并非总位于物体表面,或者某个面的中心。针对这种情况,我建议你改用3D游标作为目标,并通过进入编辑模式,灵活运用吸附和`Shift + S`菜单,来将游标放置在正确的位置。
|
||||
|
||||
然后是选择摄像机的旋转角度。我们提供了8种游戏内预设角度,分别对应90度和45度的各4种。此外如果这些预设角度不能满足你的需求,你还可以设置自定义角度。
|
||||
|
||||
最后是选择摄像机视角,分为Ordinary(常规视角),Lift(按住空格键的视角)和Easter Egg(彩蛋视角)三种。
|
||||
|
2227
i18n/blender.pot
2293
i18n/zh_HANS.po
@ -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)
|
||||
|
@ -2,12 +2,13 @@ import json, logging
|
||||
from pathlib import Path
|
||||
import common
|
||||
from common import AssetKind
|
||||
import json5
|
||||
|
||||
|
||||
def _compress_json(src_file: Path, dst_file: Path) -> None:
|
||||
# load data first
|
||||
with open(src_file, 'r', encoding='utf-8') as f:
|
||||
loaded_prototypes = json.load(f)
|
||||
loaded_prototypes = json5.load(f)
|
||||
|
||||
# save result with compress config
|
||||
with open(dst_file, 'w', encoding='utf-8') as f:
|
||||
@ -24,13 +25,14 @@ def build_jsons() -> None:
|
||||
raw_jsons_dir = common.get_raw_assets_folder(AssetKind.Jsons)
|
||||
plg_jsons_dir = common.get_plugin_assets_folder(AssetKind.Jsons)
|
||||
|
||||
for raw_json_file in raw_jsons_dir.glob('*.json'):
|
||||
for raw_json_file in raw_jsons_dir.glob('*.json5'):
|
||||
# Skip non-file.
|
||||
if not raw_json_file.is_file():
|
||||
continue
|
||||
|
||||
# Build final path
|
||||
plg_json_file = plg_jsons_dir / raw_json_file.relative_to(raw_jsons_dir)
|
||||
plg_json_file = plg_json_file.with_suffix('.json')
|
||||
|
||||
# Show message
|
||||
logging.info(f'Compressing {raw_json_file} -> {plg_json_file}')
|
||||
|
@ -1,4 +1,4 @@
|
||||
import os, typing, logging, enum
|
||||
import logging, enum, typing
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
|
@ -1,88 +1,104 @@
|
||||
import json, logging, typing, itertools
|
||||
import logging, typing, itertools
|
||||
from pathlib import Path
|
||||
import common, bme
|
||||
from common import AssetKind
|
||||
import pydantic, polib
|
||||
import pydantic, polib, json5
|
||||
|
||||
## YYC MARK:
|
||||
# This translation context string prefix is cpoied from UTIL_translation.py.
|
||||
# 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 = json.load(f)
|
||||
prototypes = bme.Prototypes.model_validate(document)
|
||||
# Extract translation
|
||||
return itertools.chain.from_iterable(_extract_prototype(prototype) for prototype in prototypes.root)
|
||||
except json.JSONDecodeError:
|
||||
logging.error(f'Can not extract translation from {json_file} due to JSON error. Please validate it first.')
|
||||
except pydantic.ValidationError:
|
||||
logging.error(f'Can not extract translation from {json_file} due to struct 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('*.json'):
|
||||
# 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()
|
||||
|
@ -3,6 +3,7 @@ name = "scripts"
|
||||
version = "1.0.0"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"json5>=0.12.0",
|
||||
"pillow==10.2.0",
|
||||
"polib>=1.2.0",
|
||||
"pydantic>=2.11.7",
|
||||
|
11
scripts/uv.lock
generated
@ -11,6 +11,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "json5"
|
||||
version = "0.12.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/12/be/c6c745ec4c4539b25a278b70e29793f10382947df0d9efba2fa09120895d/json5-0.12.0.tar.gz", hash = "sha256:0b4b6ff56801a1c7dc817b0241bca4ce474a0e6a163bfef3fc594d3fd263ff3a", size = 51907, upload-time = "2025-04-03T16:33:13.201Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/41/9f/3500910d5a98549e3098807493851eeef2b89cdd3032227558a104dfe926/json5-0.12.0-py3-none-any.whl", hash = "sha256:6d37aa6c08b0609f16e1ec5ff94697e2cbbfbad5ac112afa05794da9ab7810db", size = 36079, upload-time = "2025-04-03T16:33:11.927Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "10.2.0"
|
||||
@ -135,6 +144,7 @@ name = "scripts"
|
||||
version = "1.0.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "json5" },
|
||||
{ name = "pillow" },
|
||||
{ name = "polib" },
|
||||
{ name = "pydantic" },
|
||||
@ -142,6 +152,7 @@ dependencies = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "json5", specifier = ">=0.12.0" },
|
||||
{ name = "pillow", specifier = "==10.2.0" },
|
||||
{ name = "polib", specifier = ">=1.2.0" },
|
||||
{ name = "pydantic", specifier = ">=2.11.7" },
|
||||
|
@ -1,43 +1,176 @@
|
||||
import json, logging, ast, typing
|
||||
import logging, ast, typing
|
||||
import common, bme
|
||||
from common import AssetKind
|
||||
import pydantic
|
||||
import pydantic, json5
|
||||
|
||||
#region Assistant Validator
|
||||
#region Assistant Checker
|
||||
|
||||
def _validate_programmable_field(probe: str) -> None:
|
||||
|
||||
def _try_add(entries: set[str], entry: str) -> bool:
|
||||
if entry in entries:
|
||||
return False
|
||||
else:
|
||||
entries.add(entry)
|
||||
return True
|
||||
|
||||
|
||||
def _check_programmable_field(probe: str) -> None:
|
||||
try:
|
||||
ast.parse(probe)
|
||||
except SyntaxError:
|
||||
logging.error(f'String {probe} may not be a valid Python statement which is not suit for programmable field.')
|
||||
|
||||
|
||||
def _validate_showcase_icon(icon_name: str) -> None:
|
||||
def _check_showcase_icon(icon_name: str) -> None:
|
||||
icon_path = common.get_raw_assets_folder(AssetKind.Icons) / 'bme' / f'{icon_name}.png'
|
||||
if not icon_path.is_file():
|
||||
logging.error(f'Icon value {icon_name} may not be valid because it do not existing.')
|
||||
logging.error(f'Showcase icon value {icon_name} may be invalid.')
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Core Validator
|
||||
|
||||
def _validate_prototype(prototype: bme.Prototype) -> None:
|
||||
pass
|
||||
|
||||
def _pre_validate_prototype(prototype: bme.Prototype, identifiers: set[str]) -> None:
|
||||
identifier = prototype.identifier
|
||||
|
||||
# Show status
|
||||
logging.info(f'Pre-checking prototype {identifier}')
|
||||
|
||||
# Check identifier and add it.
|
||||
if not _try_add(identifiers, identifier):
|
||||
logging.error(f'Identifier {identifier} is already registered.')
|
||||
|
||||
|
||||
def _validate_showcase(showcase: bme.Showcase, variables: set[str]) -> None:
|
||||
# I18N Module Req:
|
||||
# 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)
|
||||
|
||||
# Check configuration list.
|
||||
for cfg in showcase.cfgs:
|
||||
# Check name
|
||||
field_name = cfg.field
|
||||
if not _try_add(variables, field_name):
|
||||
logging.error(f'Field {field_name} is already registered.')
|
||||
|
||||
# I18N Module Req:
|
||||
# The title and desc of cfg should not be empty.
|
||||
# And they are should not be the same string.
|
||||
if len(cfg.title) == 0:
|
||||
logging.error('The title of showcase configuration entry should not be empty.')
|
||||
if len(cfg.desc) == 0:
|
||||
logging.error('The description of showcase configuration entry should not be empty.')
|
||||
if cfg.title == cfg.desc:
|
||||
logging.error('The title of showcase configuration entry and its description should not be same string.')
|
||||
|
||||
# Check programmable field
|
||||
_check_programmable_field(cfg.default)
|
||||
|
||||
|
||||
def _validate_params(params: list[bme.Param], variables: set[str]) -> None:
|
||||
for param in params:
|
||||
# Check name
|
||||
field_name = param.field
|
||||
if not _try_add(variables, field_name):
|
||||
logging.error(f'Field {field_name} is already registered.')
|
||||
|
||||
# Check programmable fields
|
||||
_check_programmable_field(param.data)
|
||||
|
||||
|
||||
def _validate_vars(vars: list[bme.Var], variables: set[str]) -> None:
|
||||
for var in vars:
|
||||
# Check name
|
||||
field_name = var.field
|
||||
if not _try_add(variables, field_name):
|
||||
logging.error(f'Field {field_name} is already registered.')
|
||||
|
||||
# Check programmable fields
|
||||
_check_programmable_field(var.data)
|
||||
|
||||
|
||||
def _validate_vertices(vertices: list[bme.Vertex]) -> None:
|
||||
for vertex in vertices:
|
||||
# Check programmable fields
|
||||
_check_programmable_field(vertex.skip)
|
||||
_check_programmable_field(vertex.data)
|
||||
|
||||
|
||||
def _validate_faces(faces: list[bme.Face], vertices_count: int) -> None:
|
||||
for face in faces:
|
||||
# The index referred in indices should not be exceed the max value of vertices count.
|
||||
for index in face.indices:
|
||||
if index >= vertices_count:
|
||||
logging.error(f'Index {index} is out of vertices range.')
|
||||
|
||||
# The size of uvs list and normals list (if existing)
|
||||
# should be equal to the size of indices list.
|
||||
edges = len(face.indices)
|
||||
if len(face.uvs) != edges:
|
||||
logging.error(f'The size of UVs list is not matched with indices.')
|
||||
if face.normals is not None and len(face.normals) != edges:
|
||||
logging.error(f'The size of Normals list is not matched with indices.')
|
||||
|
||||
# Check programmable fields
|
||||
_check_programmable_field(face.skip)
|
||||
_check_programmable_field(face.texture)
|
||||
for uv in face.uvs:
|
||||
_check_programmable_field(uv)
|
||||
if face.normals is not None:
|
||||
for normal in face.normals:
|
||||
_check_programmable_field(normal)
|
||||
|
||||
|
||||
def _validate_instances(instances: list[bme.Instance], identifiers: set[str]) -> None:
|
||||
for instance in instances:
|
||||
# The reference of identifier should be existing.
|
||||
referred_identifier = instance.identifier
|
||||
if referred_identifier not in identifiers:
|
||||
logging.error(f'The identifier {referred_identifier} referred in instance is not existing.')
|
||||
|
||||
# Check programmable fields
|
||||
_check_programmable_field(instance.skip)
|
||||
for v in instance.params.values():
|
||||
_check_programmable_field(v)
|
||||
_check_programmable_field(instance.transform)
|
||||
|
||||
|
||||
def _validate_prototype(prototype: bme.Prototype, identifiers: set[str]) -> None:
|
||||
# Show status
|
||||
logging.info(f'Checking prototype {prototype.identifier}')
|
||||
|
||||
# A set of all variable names registered in this prototypes
|
||||
variables: set[str] = set()
|
||||
|
||||
# Check fields
|
||||
if prototype.showcase is not None:
|
||||
_validate_showcase(prototype.showcase, variables)
|
||||
_validate_params(prototype.params, variables)
|
||||
_check_programmable_field(prototype.skip)
|
||||
_validate_vars(prototype.vars, variables)
|
||||
_validate_vertices(prototype.vertices)
|
||||
_validate_faces(prototype.faces, len(prototype.vertices))
|
||||
_validate_instances(prototype.instances, identifiers)
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
# 把提取JSON翻译的要求写入到验证中:
|
||||
# - Showcase::Cfgs::Title或Desc不能为空。
|
||||
# - Showcase::Cfgs::Title和Showcase::Cfgs::Desc不能重复
|
||||
|
||||
|
||||
def validate_jsons() -> None:
|
||||
raw_jsons_dir = common.get_raw_assets_folder(AssetKind.Jsons)
|
||||
|
||||
# Load all prototypes and check their basic format
|
||||
prototypes: list[bme.Prototype] = []
|
||||
for raw_json_file in raw_jsons_dir.glob('*.json'):
|
||||
for raw_json_file in raw_jsons_dir.glob('*.json5'):
|
||||
# Skip non-file
|
||||
if not raw_json_file.is_file():
|
||||
continue
|
||||
@ -48,28 +181,26 @@ def validate_jsons() -> None:
|
||||
# Load prototypes
|
||||
try:
|
||||
with open(raw_json_file, 'r', encoding='utf-8') as f:
|
||||
docuement = json.load(f)
|
||||
docuement = json5.load(f)
|
||||
file_prototypes = bme.Prototypes.model_validate(docuement)
|
||||
except json.JSONDecodeError as e:
|
||||
logging.error(f'File {raw_json_file} is not a valid JSON file. Reason: {e}')
|
||||
except pydantic.ValidationError as e:
|
||||
logging.error(f'JSON file {raw_json_file} lose essential fields. Detail: {e}')
|
||||
except (ValueError, UnicodeDecodeError) as e:
|
||||
logging.error(f'File {raw_json_file} is not a valid JSON5 file. Reason: {e}')
|
||||
|
||||
# Append all prototypes into list
|
||||
prototypes += file_prototypes.root
|
||||
|
||||
# Collect identifier and check identifier first.
|
||||
# Pre-validate first to collect identifier and check identifier first.
|
||||
# We need collect it first because "instances" field need it to check the validation of identifier.
|
||||
identifiers: set[str] = set()
|
||||
for prototype in prototypes:
|
||||
identifier = prototype.identifier
|
||||
if prototype.identifier in identifiers:
|
||||
logging.error(f'Identifier {identifier} is registered more than once.')
|
||||
else:
|
||||
identifiers.add(identifier)
|
||||
_pre_validate_prototype(prototype, identifiers)
|
||||
|
||||
# Start custom validation
|
||||
for protype in prototypes:
|
||||
_validate_prototype(prototype)
|
||||
for prototype in prototypes:
|
||||
_validate_prototype(prototype, identifiers)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
common.setup_logging()
|
||||
|