Compare commits
26 Commits
0ae95e927f
...
v4.3-alpha
Author | SHA1 | Date | |
---|---|---|---|
2b2b18cfa4 | |||
b19800e37f | |||
e14729500c | |||
48bfc54830 | |||
7e74e42bd7 | |||
96a81b165b | |||
0681f0d240 | |||
d700f1276a | |||
3bea3d67b9 | |||
ec41b7553a | |||
9e2539499e | |||
3a5cd1c937 | |||
1afa5f9893 | |||
1383e87104 | |||
a2b8f41a21 | |||
93f23abeb9 | |||
4ba3ff9e5a | |||
a9a889a8fd | |||
fc34b19a42 | |||
9e65d258d7 | |||
ab266a07fb | |||
61c7709b97 | |||
1044d4b450 | |||
54edc4dab7 | |||
f40efb0467 | |||
10de948a79 |
Binary file not shown.
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 30 KiB |
Binary file not shown.
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 31 KiB |
Binary file not shown.
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 42 KiB |
@ -84,6 +84,7 @@
|
|||||||
"identifier": "floor_normal_1x1",
|
"identifier": "floor_normal_1x1",
|
||||||
"showcase": {
|
"showcase": {
|
||||||
"title": "Normal 1x1",
|
"title": "Normal 1x1",
|
||||||
|
"category": "1x1 Blocks",
|
||||||
"icon": "Normal1x1",
|
"icon": "Normal1x1",
|
||||||
"type": "floor",
|
"type": "floor",
|
||||||
"cfgs": [
|
"cfgs": [
|
||||||
@ -134,6 +135,7 @@
|
|||||||
"identifier": "floor_sink_1x1",
|
"identifier": "floor_sink_1x1",
|
||||||
"showcase": {
|
"showcase": {
|
||||||
"title": "Sink 1x1",
|
"title": "Sink 1x1",
|
||||||
|
"category": "1x1 Blocks",
|
||||||
"icon": "Sink1x1",
|
"icon": "Sink1x1",
|
||||||
"type": "floor",
|
"type": "floor",
|
||||||
"cfgs": [
|
"cfgs": [
|
@ -49,6 +49,7 @@
|
|||||||
"identifier": "floor_normal_border",
|
"identifier": "floor_normal_border",
|
||||||
"showcase": {
|
"showcase": {
|
||||||
"title": "Normal Border",
|
"title": "Normal Border",
|
||||||
|
"category": "Borders",
|
||||||
"icon": "NormalBorder",
|
"icon": "NormalBorder",
|
||||||
"type": "floor",
|
"type": "floor",
|
||||||
"cfgs": [
|
"cfgs": [
|
||||||
@ -112,6 +113,7 @@
|
|||||||
"identifier": "floor_sink_border",
|
"identifier": "floor_sink_border",
|
||||||
"showcase": {
|
"showcase": {
|
||||||
"title": "Sink Border",
|
"title": "Sink Border",
|
||||||
|
"category": "Borders",
|
||||||
"icon": "SinkBorder",
|
"icon": "SinkBorder",
|
||||||
"type": "floor",
|
"type": "floor",
|
||||||
"cfgs": [
|
"cfgs": [
|
||||||
@ -175,6 +177,7 @@
|
|||||||
"identifier": "floor_ribbon_border",
|
"identifier": "floor_ribbon_border",
|
||||||
"showcase": {
|
"showcase": {
|
||||||
"title": "Ribbon Border",
|
"title": "Ribbon Border",
|
||||||
|
"category": "Borders",
|
||||||
"icon": "RibbonBorder",
|
"icon": "RibbonBorder",
|
||||||
"type": "floor",
|
"type": "floor",
|
||||||
"cfgs": [
|
"cfgs": [
|
@ -1,4 +1,12 @@
|
|||||||
[
|
[
|
||||||
|
// One of Chris suggested more vanilla prototypes.
|
||||||
|
// This prototype represent a half of a normal, sink or double ribbon border,
|
||||||
|
// which looks like trapezoid from top to bottom.
|
||||||
|
//
|
||||||
|
// The bottom edge of trapezoid is from origin to +X with `long_edge_length` length.
|
||||||
|
// The length of top edge is `short_edge_length` and it just like moving bottom edge to +Y direction.
|
||||||
|
// The offset between top edge and bottom edge is always 2.5.
|
||||||
|
// The distance from the closest point of top edge, to Y axis is `short_edge_offset`.
|
||||||
{
|
{
|
||||||
"identifier": "cv_trapezoid_side",
|
"identifier": "cv_trapezoid_side",
|
||||||
"showcase": null,
|
"showcase": null,
|
||||||
@ -153,6 +161,11 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
// Same as previous one, but looks like triangle from top to bottom.
|
||||||
|
//
|
||||||
|
// The bottom edge is from origin to +X with `edge_length` length.
|
||||||
|
// The tip is going to +Y.
|
||||||
|
// The height of this triangle is always 2.5 and the offset between tip and Y axis is `tip_offset`.
|
||||||
{
|
{
|
||||||
"identifier": "cv_triangle_side",
|
"identifier": "cv_triangle_side",
|
||||||
"showcase": null,
|
"showcase": null,
|
@ -149,6 +149,7 @@
|
|||||||
"identifier": "floor_normal_inner_corner",
|
"identifier": "floor_normal_inner_corner",
|
||||||
"showcase": {
|
"showcase": {
|
||||||
"title": "Normal Inner Corner",
|
"title": "Normal Inner Corner",
|
||||||
|
"category": "Half Block Corners",
|
||||||
"icon": "NormalInnerCorner",
|
"icon": "NormalInnerCorner",
|
||||||
"type": "floor",
|
"type": "floor",
|
||||||
"cfgs": [
|
"cfgs": [
|
||||||
@ -201,6 +202,7 @@
|
|||||||
"identifier": "floor_sink_inner_corner",
|
"identifier": "floor_sink_inner_corner",
|
||||||
"showcase": {
|
"showcase": {
|
||||||
"title": "Sink Inner Corner",
|
"title": "Sink Inner Corner",
|
||||||
|
"category": "Half Block Corners",
|
||||||
"icon": "SinkInnerCorner",
|
"icon": "SinkInnerCorner",
|
||||||
"type": "floor",
|
"type": "floor",
|
||||||
"cfgs": [
|
"cfgs": [
|
||||||
@ -253,6 +255,7 @@
|
|||||||
"identifier": "floor_ribbon_inner_corner",
|
"identifier": "floor_ribbon_inner_corner",
|
||||||
"showcase": {
|
"showcase": {
|
||||||
"title": "Ribbon Inner Corner",
|
"title": "Ribbon Inner Corner",
|
||||||
|
"category": "Half Block Corners",
|
||||||
"icon": "RibbonInnerCorner",
|
"icon": "RibbonInnerCorner",
|
||||||
"type": "floor",
|
"type": "floor",
|
||||||
"cfgs": [
|
"cfgs": [
|
||||||
@ -305,6 +308,7 @@
|
|||||||
"identifier": "floor_normal_outter_corner",
|
"identifier": "floor_normal_outter_corner",
|
||||||
"showcase": {
|
"showcase": {
|
||||||
"title": "Normal Outter Corner",
|
"title": "Normal Outter Corner",
|
||||||
|
"category": "Half Block Corners",
|
||||||
"icon": "NormalOutterCorner",
|
"icon": "NormalOutterCorner",
|
||||||
"type": "floor",
|
"type": "floor",
|
||||||
"cfgs": [
|
"cfgs": [
|
||||||
@ -357,6 +361,7 @@
|
|||||||
"identifier": "floor_sink_outter_corner",
|
"identifier": "floor_sink_outter_corner",
|
||||||
"showcase": {
|
"showcase": {
|
||||||
"title": "Sink Outter Corner",
|
"title": "Sink Outter Corner",
|
||||||
|
"category": "Half Block Corners",
|
||||||
"icon": "SinkOutterCorner",
|
"icon": "SinkOutterCorner",
|
||||||
"type": "floor",
|
"type": "floor",
|
||||||
"cfgs": [
|
"cfgs": [
|
||||||
@ -409,6 +414,7 @@
|
|||||||
"identifier": "floor_ribbon_outter_corner",
|
"identifier": "floor_ribbon_outter_corner",
|
||||||
"showcase": {
|
"showcase": {
|
||||||
"title": "Ribbon Outter Corner",
|
"title": "Ribbon Outter Corner",
|
||||||
|
"category": "Half Block Corners",
|
||||||
"icon": "RibbonOutterCorner",
|
"icon": "RibbonOutterCorner",
|
||||||
"type": "floor",
|
"type": "floor",
|
||||||
"cfgs": [
|
"cfgs": [
|
@ -228,6 +228,7 @@
|
|||||||
"identifier": "floor_normal_l_crossing",
|
"identifier": "floor_normal_l_crossing",
|
||||||
"showcase": {
|
"showcase": {
|
||||||
"title": "Normal L Crossing",
|
"title": "Normal L Crossing",
|
||||||
|
"category": "Floor Crossings",
|
||||||
"icon": "NormalLCrossing",
|
"icon": "NormalLCrossing",
|
||||||
"type": "floor",
|
"type": "floor",
|
||||||
"cfgs": [
|
"cfgs": [
|
||||||
@ -278,6 +279,7 @@
|
|||||||
"identifier": "floor_sink_l_crossing",
|
"identifier": "floor_sink_l_crossing",
|
||||||
"showcase": {
|
"showcase": {
|
||||||
"title": "Sink L Crossing",
|
"title": "Sink L Crossing",
|
||||||
|
"category": "Floor Crossings",
|
||||||
"icon": "SinkLCrossing",
|
"icon": "SinkLCrossing",
|
||||||
"type": "floor",
|
"type": "floor",
|
||||||
"cfgs": [
|
"cfgs": [
|
||||||
@ -328,6 +330,7 @@
|
|||||||
"identifier": "floor_normal_t_crossing",
|
"identifier": "floor_normal_t_crossing",
|
||||||
"showcase": {
|
"showcase": {
|
||||||
"title": "Normal T Crossing",
|
"title": "Normal T Crossing",
|
||||||
|
"category": "Floor Crossings",
|
||||||
"icon": "NormalTCrossing",
|
"icon": "NormalTCrossing",
|
||||||
"type": "floor",
|
"type": "floor",
|
||||||
"cfgs": [
|
"cfgs": [
|
||||||
@ -378,6 +381,7 @@
|
|||||||
"identifier": "floor_sink_t_crossing",
|
"identifier": "floor_sink_t_crossing",
|
||||||
"showcase": {
|
"showcase": {
|
||||||
"title": "Sink T Crossing",
|
"title": "Sink T Crossing",
|
||||||
|
"category": "Floor Crossings",
|
||||||
"icon": "SinkTCrossing",
|
"icon": "SinkTCrossing",
|
||||||
"type": "floor",
|
"type": "floor",
|
||||||
"cfgs": [
|
"cfgs": [
|
||||||
@ -428,6 +432,7 @@
|
|||||||
"identifier": "floor_normal_x_crossing",
|
"identifier": "floor_normal_x_crossing",
|
||||||
"showcase": {
|
"showcase": {
|
||||||
"title": "Normal X Crossing",
|
"title": "Normal X Crossing",
|
||||||
|
"category": "Floor Crossings",
|
||||||
"icon": "NormalXCrossing",
|
"icon": "NormalXCrossing",
|
||||||
"type": "floor",
|
"type": "floor",
|
||||||
"cfgs": [
|
"cfgs": [
|
||||||
@ -478,6 +483,7 @@
|
|||||||
"identifier": "floor_sink_x_crossing",
|
"identifier": "floor_sink_x_crossing",
|
||||||
"showcase": {
|
"showcase": {
|
||||||
"title": "Sink X Crossing",
|
"title": "Sink X Crossing",
|
||||||
|
"category": "Floor Crossings",
|
||||||
"icon": "SinkXCrossing",
|
"icon": "SinkXCrossing",
|
||||||
"type": "floor",
|
"type": "floor",
|
||||||
"cfgs": [
|
"cfgs": [
|
@ -3,6 +3,7 @@
|
|||||||
"identifier": "floor_flat",
|
"identifier": "floor_flat",
|
||||||
"showcase": {
|
"showcase": {
|
||||||
"title": "Flat",
|
"title": "Flat",
|
||||||
|
"category": "Miscellaneous",
|
||||||
"icon": "Flat",
|
"icon": "Flat",
|
||||||
"type": "floor",
|
"type": "floor",
|
||||||
"cfgs": [
|
"cfgs": [
|
||||||
@ -36,7 +37,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "is_sink_",
|
"field": "is_sink_",
|
||||||
"type": "float",
|
"type": "bool",
|
||||||
"title": "Is Sink",
|
"title": "Is Sink",
|
||||||
"desc": "Whether this flat floor is used for sink floor.",
|
"desc": "Whether this flat floor is used for sink floor.",
|
||||||
"default": "False"
|
"default": "False"
|
@ -116,6 +116,7 @@
|
|||||||
"identifier": "floor_normal_platform",
|
"identifier": "floor_normal_platform",
|
||||||
"showcase": {
|
"showcase": {
|
||||||
"title": "Normal Platform",
|
"title": "Normal Platform",
|
||||||
|
"category": "Platforms",
|
||||||
"icon": "NormalPlatform",
|
"icon": "NormalPlatform",
|
||||||
"type": "floor",
|
"type": "floor",
|
||||||
"cfgs": [
|
"cfgs": [
|
||||||
@ -191,6 +192,7 @@
|
|||||||
"identifier": "floor_sink_platform",
|
"identifier": "floor_sink_platform",
|
||||||
"showcase": {
|
"showcase": {
|
||||||
"title": "Sink Platform",
|
"title": "Sink Platform",
|
||||||
|
"category": "Platforms",
|
||||||
"icon": "SinkPlatform",
|
"icon": "SinkPlatform",
|
||||||
"type": "floor",
|
"type": "floor",
|
||||||
"cfgs": [
|
"cfgs": [
|
||||||
@ -266,6 +268,7 @@
|
|||||||
"identifier": "floor_ribbon_platform",
|
"identifier": "floor_ribbon_platform",
|
||||||
"showcase": {
|
"showcase": {
|
||||||
"title": "Ribbon Platform",
|
"title": "Ribbon Platform",
|
||||||
|
"category": "Platforms",
|
||||||
"icon": "RibbonPlatform",
|
"icon": "RibbonPlatform",
|
||||||
"type": "floor",
|
"type": "floor",
|
||||||
"cfgs": [
|
"cfgs": [
|
@ -3,6 +3,7 @@
|
|||||||
"identifier": "floor_normal_straight",
|
"identifier": "floor_normal_straight",
|
||||||
"showcase": {
|
"showcase": {
|
||||||
"title": "Normal Floor",
|
"title": "Normal Floor",
|
||||||
|
"category": "Floors",
|
||||||
"icon": "NormalFloor",
|
"icon": "NormalFloor",
|
||||||
"type": "floor",
|
"type": "floor",
|
||||||
"cfgs": [
|
"cfgs": [
|
||||||
@ -142,6 +143,7 @@
|
|||||||
"identifier": "floor_sink_straight",
|
"identifier": "floor_sink_straight",
|
||||||
"showcase": {
|
"showcase": {
|
||||||
"title": "Sink Floor",
|
"title": "Sink Floor",
|
||||||
|
"category": "Floors",
|
||||||
"icon": "SinkFloor",
|
"icon": "SinkFloor",
|
||||||
"type": "floor",
|
"type": "floor",
|
||||||
"cfgs": [
|
"cfgs": [
|
@ -1,4 +1,5 @@
|
|||||||
[
|
[
|
||||||
|
// The shared template prototype used by all floor terminals.
|
||||||
{
|
{
|
||||||
"identifier": "raw_floor_terminal",
|
"identifier": "raw_floor_terminal",
|
||||||
"showcase": null,
|
"showcase": null,
|
||||||
@ -22,26 +23,30 @@
|
|||||||
"faces": [],
|
"faces": [],
|
||||||
"instances": [
|
"instances": [
|
||||||
{
|
{
|
||||||
"identifier": "cv_triangle_side",
|
"identifier": "cv_trapezoid_side",
|
||||||
"skip": "False",
|
"skip": "False",
|
||||||
"params": {
|
"params": {
|
||||||
"edge_length": "2.5",
|
"long_edge_length": "5.0",
|
||||||
"tip_offset": "2.5",
|
"short_edge_offset": "2.5",
|
||||||
|
"short_edge_length": "2.5",
|
||||||
"height": "height",
|
"height": "height",
|
||||||
"face": "(face[0], False, False, face[3], face[4], None)",
|
"face": "(face[0], False, False, face[3], face[4], False)",
|
||||||
"is_sink": "is_sink"
|
"is_sink": "is_sink",
|
||||||
|
"is_ribbon": "False"
|
||||||
},
|
},
|
||||||
"transform": "ident()"
|
"transform": "ident()"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"identifier": "cv_triangle_side",
|
"identifier": "cv_trapezoid_side",
|
||||||
"skip": "False",
|
"skip": "False",
|
||||||
"params": {
|
"params": {
|
||||||
"edge_length": "2.5",
|
"long_edge_length": "5.0",
|
||||||
"tip_offset": "2.5",
|
"short_edge_offset": "2.5",
|
||||||
|
"short_edge_length": "2.5",
|
||||||
"height": "height",
|
"height": "height",
|
||||||
"face": "(face[0], False, False, face[3], face[5], None)",
|
"face": "(face[0], False, False, face[3], face[5], False)",
|
||||||
"is_sink": "is_sink"
|
"is_sink": "is_sink",
|
||||||
|
"is_ribbon": "False"
|
||||||
},
|
},
|
||||||
"transform": "move(0, 5, 0) @ scale(1, -1, 1)"
|
"transform": "move(0, 5, 0) @ scale(1, -1, 1)"
|
||||||
},
|
},
|
||||||
@ -61,7 +66,7 @@
|
|||||||
"identifier": "floor_rectangle_bottom",
|
"identifier": "floor_rectangle_bottom",
|
||||||
"skip": "not face[1]",
|
"skip": "not face[1]",
|
||||||
"params": {
|
"params": {
|
||||||
"length": "2.5",
|
"length": "5",
|
||||||
"width": "5"
|
"width": "5"
|
||||||
},
|
},
|
||||||
"transform": "move(0, 0, -height)"
|
"transform": "move(0, 0, -height)"
|
||||||
@ -72,6 +77,7 @@
|
|||||||
"identifier": "floor_normal_terminal",
|
"identifier": "floor_normal_terminal",
|
||||||
"showcase": {
|
"showcase": {
|
||||||
"title": "Normal Floor Terminal",
|
"title": "Normal Floor Terminal",
|
||||||
|
"category": "Floors",
|
||||||
"icon": "NormalFloorTerminal",
|
"icon": "NormalFloorTerminal",
|
||||||
"type": "floor",
|
"type": "floor",
|
||||||
"cfgs": [
|
"cfgs": [
|
||||||
@ -122,6 +128,7 @@
|
|||||||
"identifier": "floor_sink_terminal",
|
"identifier": "floor_sink_terminal",
|
||||||
"showcase": {
|
"showcase": {
|
||||||
"title": "Sink Floor Terminal",
|
"title": "Sink Floor Terminal",
|
||||||
|
"category": "Floors",
|
||||||
"icon": "SinkFloorTerminal",
|
"icon": "SinkFloorTerminal",
|
||||||
"type": "floor",
|
"type": "floor",
|
||||||
"cfgs": [
|
"cfgs": [
|
@ -137,6 +137,7 @@
|
|||||||
"identifier": "wood_trafo",
|
"identifier": "wood_trafo",
|
||||||
"showcase": {
|
"showcase": {
|
||||||
"title": "Wood Trafo",
|
"title": "Wood Trafo",
|
||||||
|
"category": "Trafo",
|
||||||
"icon": "WoodTrafo",
|
"icon": "WoodTrafo",
|
||||||
"type": "floor",
|
"type": "floor",
|
||||||
"cfgs": [
|
"cfgs": [
|
||||||
@ -187,6 +188,7 @@
|
|||||||
"identifier": "stone_trafo",
|
"identifier": "stone_trafo",
|
||||||
"showcase": {
|
"showcase": {
|
||||||
"title": "Stone Trafo",
|
"title": "Stone Trafo",
|
||||||
|
"category": "Trafo",
|
||||||
"icon": "StoneTrafo",
|
"icon": "StoneTrafo",
|
||||||
"type": "floor",
|
"type": "floor",
|
||||||
"cfgs": [
|
"cfgs": [
|
||||||
@ -237,6 +239,7 @@
|
|||||||
"identifier": "paper_trafo",
|
"identifier": "paper_trafo",
|
||||||
"showcase": {
|
"showcase": {
|
||||||
"title": "Paper Trafo",
|
"title": "Paper Trafo",
|
||||||
|
"category": "Trafo",
|
||||||
"icon": "PaperTrafo",
|
"icon": "PaperTrafo",
|
||||||
"type": "floor",
|
"type": "floor",
|
||||||
"cfgs": [
|
"cfgs": [
|
@ -111,6 +111,7 @@
|
|||||||
"identifier": "floor_transition",
|
"identifier": "floor_transition",
|
||||||
"showcase": {
|
"showcase": {
|
||||||
"title": "Transition",
|
"title": "Transition",
|
||||||
|
"category": "Miscellaneous",
|
||||||
"icon": "Transition",
|
"icon": "Transition",
|
||||||
"type": "floor",
|
"type": "floor",
|
||||||
"cfgs": [
|
"cfgs": [
|
||||||
@ -191,6 +192,7 @@
|
|||||||
"identifier": "floor_narrow_transition",
|
"identifier": "floor_narrow_transition",
|
||||||
"showcase": {
|
"showcase": {
|
||||||
"title": "Narrow Transition",
|
"title": "Narrow Transition",
|
||||||
|
"category": "Miscellaneous",
|
||||||
"icon": "NarrowTransition",
|
"icon": "NarrowTransition",
|
||||||
"type": "floor",
|
"type": "floor",
|
||||||
"cfgs": [
|
"cfgs": [
|
@ -3,6 +3,7 @@
|
|||||||
"identifier": "floor_wide_straight",
|
"identifier": "floor_wide_straight",
|
||||||
"showcase": {
|
"showcase": {
|
||||||
"title": "Wide Floor",
|
"title": "Wide Floor",
|
||||||
|
"category": "Wide Floors",
|
||||||
"icon": "WideFloor",
|
"icon": "WideFloor",
|
||||||
"type": "floor",
|
"type": "floor",
|
||||||
"cfgs": [
|
"cfgs": [
|
||||||
@ -106,6 +107,7 @@
|
|||||||
"identifier": "floor_wide_terminal",
|
"identifier": "floor_wide_terminal",
|
||||||
"showcase": {
|
"showcase": {
|
||||||
"title": "Wide Floor Terminal",
|
"title": "Wide Floor Terminal",
|
||||||
|
"category": "Wide Floors",
|
||||||
"icon": "WideFloorTerminal",
|
"icon": "WideFloorTerminal",
|
||||||
"type": "floor",
|
"type": "floor",
|
||||||
"cfgs": [
|
"cfgs": [
|
||||||
@ -166,34 +168,50 @@
|
|||||||
"transform": "rot(0, 0, 90) @ scale(1, -1, 1)"
|
"transform": "rot(0, 0, 90) @ scale(1, -1, 1)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"identifier": "cv_triangle_side",
|
"identifier": "cv_trapezoid_side",
|
||||||
"skip": "False",
|
"skip": "False",
|
||||||
"params": {
|
"params": {
|
||||||
"edge_length": "2.5",
|
"long_edge_length": "5.0",
|
||||||
"tip_offset": "2.5",
|
"short_edge_offset": "2.5",
|
||||||
|
"short_edge_length": "2.5",
|
||||||
"height": "height",
|
"height": "height",
|
||||||
"face": "(face[0], False, False, face[3], face[4], None)",
|
"face": "(face[0], False, False, face[3], face[4], False)",
|
||||||
"is_sink": "True"
|
"is_sink": "True",
|
||||||
|
"is_ribbon": "False"
|
||||||
},
|
},
|
||||||
"transform": "ident()"
|
"transform": "ident()"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"identifier": "cv_triangle_side",
|
"identifier": "cv_trapezoid_side",
|
||||||
"skip": "False",
|
"skip": "False",
|
||||||
"params": {
|
"params": {
|
||||||
"edge_length": "2.5",
|
"long_edge_length": "5.0",
|
||||||
"tip_offset": "2.5",
|
"short_edge_offset": "2.5",
|
||||||
|
"short_edge_length": "2.5",
|
||||||
"height": "height",
|
"height": "height",
|
||||||
"face": "(face[0], False, False, face[3], face[5], None)",
|
"face": "(face[0], False, False, face[3], face[5], False)",
|
||||||
"is_sink": "True"
|
"is_sink": "True",
|
||||||
|
"is_ribbon": "False"
|
||||||
},
|
},
|
||||||
"transform": "move(0, width + 5, 0) @ scale(1, -1, 1)"
|
"transform": "move(0, width + 5, 0) @ scale(1, -1, 1)"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identifier": "floor_flat",
|
||||||
|
"skip": "False",
|
||||||
|
"params": {
|
||||||
|
"height": "height",
|
||||||
|
"length": "2.5",
|
||||||
|
"width": "width",
|
||||||
|
"face": "(face[0], False, False, face[3], False, False)",
|
||||||
|
"is_sink": "True"
|
||||||
|
},
|
||||||
|
"transform": "move(2.5, 2.5, 0)"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identifier": "floor_rectangle_bottom",
|
"identifier": "floor_rectangle_bottom",
|
||||||
"skip": "not face[1]",
|
"skip": "not face[1]",
|
||||||
"params": {
|
"params": {
|
||||||
"length": "2.5",
|
"length": "5",
|
||||||
"width": "5 + width"
|
"width": "5 + width"
|
||||||
},
|
},
|
||||||
"transform": "move(0, 0, -height)"
|
"transform": "move(0, 0, -height)"
|
||||||
@ -204,6 +222,7 @@
|
|||||||
"identifier": "floor_wide_l_crossing",
|
"identifier": "floor_wide_l_crossing",
|
||||||
"showcase": {
|
"showcase": {
|
||||||
"title": "Wide Floor L Crossing",
|
"title": "Wide Floor L Crossing",
|
||||||
|
"category": "Wide Floors",
|
||||||
"icon": "WideLCrossing",
|
"icon": "WideLCrossing",
|
||||||
"type": "floor",
|
"type": "floor",
|
||||||
"cfgs": [
|
"cfgs": [
|
||||||
@ -336,6 +355,7 @@
|
|||||||
"identifier": "floor_wide_t_crossing",
|
"identifier": "floor_wide_t_crossing",
|
||||||
"showcase": {
|
"showcase": {
|
||||||
"title": "Wide Floor T Crossing",
|
"title": "Wide Floor T Crossing",
|
||||||
|
"category": "Wide Floors",
|
||||||
"icon": "WideTCrossing",
|
"icon": "WideTCrossing",
|
||||||
"type": "floor",
|
"type": "floor",
|
||||||
"cfgs": [
|
"cfgs": [
|
||||||
@ -459,6 +479,7 @@
|
|||||||
"identifier": "floor_wide_x_crossing",
|
"identifier": "floor_wide_x_crossing",
|
||||||
"showcase": {
|
"showcase": {
|
||||||
"title": "Wide Floor X Crossing",
|
"title": "Wide Floor X Crossing",
|
||||||
|
"category": "Wide Floors",
|
||||||
"icon": "WideXCrossing",
|
"icon": "WideXCrossing",
|
||||||
"type": "floor",
|
"type": "floor",
|
||||||
"cfgs": [
|
"cfgs": [
|
@ -5,7 +5,7 @@ from . import UTIL_functions, UTIL_translation, UTIL_bme
|
|||||||
|
|
||||||
#region BME Adder
|
#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):
|
class BBP_PG_bme_adder_cfgs(bpy.types.PropertyGroup):
|
||||||
prop_int: bpy.props.IntProperty(
|
prop_int: bpy.props.IntProperty(
|
||||||
@ -37,39 +37,44 @@ class BBP_OT_add_bme_struct(bpy.types.Operator):
|
|||||||
bl_options = {'REGISTER', 'UNDO'}
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
bl_translation_context = 'BBP_OT_add_bme_struct'
|
bl_translation_context = 'BBP_OT_add_bme_struct'
|
||||||
|
|
||||||
## There is a compromise due to the shitty Blender design.
|
# YYC MARK:
|
||||||
#
|
# ===== 20231217 =====
|
||||||
# The passed `self` of Blender Property update function is not the instance of operator,
|
# There is a compromise due to the shitty Blender design.
|
||||||
# but a simple OperatorProperties.
|
# The passed `self` of Blender Property update function is not the instance of operator,
|
||||||
# It mean that I can not visit the full operator, only what I can do is visit existing
|
# but a simple OperatorProperties.
|
||||||
# Blender properties.
|
# 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.
|
# So these is the solution about generating cache list according to the change of bme struct type.
|
||||||
# The "outdated" flags is not showen and not saved.
|
# First, update function will only set a "outdated" flag for operator which is a pre-registered Blender property.
|
||||||
# Then call a internal cache list update function at the begin of `invoke`, `execute` and `draw`.
|
# The "outdated" flags is not showen and not saved.
|
||||||
# In this internal cache list updator, check "outdated" flag first, if cache is outdated, update and reset flag.
|
# Then call a internal cache list update function at the begin of `invoke`, `execute` and `draw`.
|
||||||
# Otherwise do nothing.
|
# 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
|
#
|
||||||
|
# Reference: https://docs.blender.org/api/current/bpy.props.html#update-example
|
||||||
## Compromise used "outdated" flag.
|
#
|
||||||
outdated_flag: bpy.props.BoolProperty(
|
# ===== 20250131 =====
|
||||||
# TR: Property not showen should not have name and desc.
|
# There is a fatal performance bug when I adding BME operator list into 3D View sidebar panels (N Menu).
|
||||||
# name = "Outdated Type",
|
# It will cause calling my Panel's `draw` function infinityly of Panel in each render tick,
|
||||||
# description = "Internal flag.",
|
# which calls `BBP_OT_add_bme_struct.draw_blc_menu` directly,
|
||||||
options = {'HIDDEN', 'SKIP_SAVE'},
|
# eat too much CPU and GPU resources and make the whole Blender be laggy.
|
||||||
default = False
|
#
|
||||||
) # type: ignore
|
# 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
|
## A BME struct cfgs descriptor cache list
|
||||||
# Not only the descriptor self, also the cfg associated index in bme_struct_cfgs
|
# 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]]
|
bme_struct_cfg_index_cache: list[tuple[UTIL_bme.PrototypeShowcaseCfgDescriptor, int]]
|
||||||
|
|
||||||
def __internal_update_bme_struct_type(self) -> None:
|
def __build_bme_struct_cfg_index_cache(self) -> None:
|
||||||
# if not outdated, skip
|
|
||||||
if not self.outdated_flag: return
|
|
||||||
|
|
||||||
# get available cfg entires
|
# get available cfg entires
|
||||||
cfgs: typing.Iterator[UTIL_bme.PrototypeShowcaseCfgDescriptor]
|
cfgs: typing.Iterator[UTIL_bme.PrototypeShowcaseCfgDescriptor]
|
||||||
cfgs = _g_EnumHelper_BmeStructType.get_bme_showcase_cfgs(
|
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):
|
for i in range(6):
|
||||||
op_cfgs_visitor[cfg_index + i].prop_bool = default_values[i]
|
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(
|
bme_struct_type: bpy.props.EnumProperty(
|
||||||
name = "Type",
|
name = "Type",
|
||||||
description = "The type of BME structure.",
|
description = "The type of BME structure.",
|
||||||
items = _g_EnumHelper_BmeStructType.generate_items(),
|
items = _g_EnumHelper_BmeStructType.generate_items(),
|
||||||
update = bme_struct_type_updated,
|
|
||||||
translation_context = 'BBP_OT_add_bme_struct/property'
|
translation_context = 'BBP_OT_add_bme_struct/property'
|
||||||
) # type: ignore
|
) # 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_translation = (0.0, 0.0, 0.0)
|
||||||
self.extra_rotation = (0.0, 0.0, 0.0)
|
self.extra_rotation = (0.0, 0.0, 0.0)
|
||||||
self.extra_scale = (1.0, 1.0, 1.0)
|
self.extra_scale = (1.0, 1.0, 1.0)
|
||||||
|
|
||||||
# create internal list
|
# create internal list
|
||||||
self.bme_struct_cfg_index_cache = []
|
self.bme_struct_cfg_index_cache = []
|
||||||
# trigger default bme struct type updator
|
# call internal builder to load prototype data inside it
|
||||||
self.bme_struct_type_updated(context)
|
self.__build_bme_struct_cfg_index_cache()
|
||||||
# call internal updator
|
|
||||||
self.__internal_update_bme_struct_type()
|
|
||||||
# run execute() function
|
# run execute() function
|
||||||
return self.execute(context)
|
return self.execute(context)
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
# call internal updator
|
|
||||||
self.__internal_update_bme_struct_type()
|
|
||||||
|
|
||||||
# create cfg visitor
|
# create cfg visitor
|
||||||
op_cfgs_visitor: UTIL_functions.CollectionVisitor[BBP_PG_bme_adder_cfgs]
|
op_cfgs_visitor: UTIL_functions.CollectionVisitor[BBP_PG_bme_adder_cfgs]
|
||||||
op_cfgs_visitor = UTIL_functions.CollectionVisitor(self.bme_struct_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'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
# call internal updator
|
|
||||||
self.__internal_update_bme_struct_type()
|
|
||||||
|
|
||||||
# start drawing
|
# start drawing
|
||||||
layout: bpy.types.UILayout = self.layout
|
layout: bpy.types.UILayout = self.layout
|
||||||
# show type
|
|
||||||
layout.prop(self, 'bme_struct_type')
|
|
||||||
|
|
||||||
# create cfg visitor
|
# create cfg visitor
|
||||||
op_cfgs_visitor: UTIL_functions.CollectionVisitor[BBP_PG_bme_adder_cfgs]
|
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:
|
case UTIL_bme.PrototypeShowcaseCfgsTypes.Float:
|
||||||
box_layout.prop(op_cfgs_visitor[cfg_index], 'prop_float', text='')
|
box_layout.prop(op_cfgs_visitor[cfg_index], 'prop_float', text='')
|
||||||
case UTIL_bme.PrototypeShowcaseCfgsTypes.Boolean:
|
case UTIL_bme.PrototypeShowcaseCfgsTypes.Boolean:
|
||||||
box_layout.prop(op_cfgs_visitor[cfg_index], 'prop_bool', text='')
|
box_layout.prop(op_cfgs_visitor[cfg_index], 'prop_bool', toggle=1, text='Yes', text_ctxt='BBP_OT_add_bme_struct/draw')
|
||||||
case UTIL_bme.PrototypeShowcaseCfgsTypes.Face:
|
case UTIL_bme.PrototypeShowcaseCfgsTypes.Face:
|
||||||
# face will show a special layout (grid view)
|
# face will show a special layout (grid view)
|
||||||
grids = box_layout.grid_flow(
|
grids = box_layout.grid_flow(
|
||||||
@ -294,16 +280,24 @@ class BBP_OT_add_bme_struct(bpy.types.Operator):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def draw_blc_menu(cls, layout: bpy.types.UILayout):
|
def draw_blc_menu(cls, layout: bpy.types.UILayout):
|
||||||
for ident in _g_EnumHelper_BmeStructType.get_bme_identifiers():
|
for category, idents in _g_EnumHelper_BmeStructType.get_bme_categories().items():
|
||||||
# draw operator
|
# draw category label
|
||||||
cop = layout.operator(
|
layout.label(text=category, text_ctxt=UTIL_translation.build_prototype_showcase_category_context())
|
||||||
cls.bl_idname,
|
|
||||||
text = _g_EnumHelper_BmeStructType.get_bme_showcase_title(ident),
|
# draw prototypes list
|
||||||
icon_value = _g_EnumHelper_BmeStructType.get_bme_showcase_icon(ident),
|
for ident in idents:
|
||||||
text_ctxt = UTIL_translation.build_prototype_showcase_context(ident)
|
# draw operator
|
||||||
)
|
cop = layout.operator(
|
||||||
# and assign its init type value
|
cls.bl_idname,
|
||||||
cop.bme_struct_type = _g_EnumHelper_BmeStructType.to_selection(ident)
|
text = _g_EnumHelper_BmeStructType.get_bme_showcase_title(ident),
|
||||||
|
icon_value = _g_EnumHelper_BmeStructType.get_bme_showcase_icon(ident),
|
||||||
|
text_ctxt = UTIL_translation.build_prototype_showcase_title_context(ident),
|
||||||
|
)
|
||||||
|
# and assign its init type value
|
||||||
|
cop.bme_struct_type = _g_EnumHelper_BmeStructType.to_selection(ident)
|
||||||
|
|
||||||
|
# draw separator
|
||||||
|
layout.separator()
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
@ -176,7 +176,7 @@ class _GeneralComponentCreator():
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Noemal Component Adder
|
#region Normal Component Adder
|
||||||
|
|
||||||
# element enum prop helper
|
# 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)
|
icon: int | None = UTIL_icons_manager.get_component_icon(elename)
|
||||||
if icon is None: return UTIL_icons_manager.get_empty_icon()
|
if icon is None: return UTIL_icons_manager.get_empty_icon()
|
||||||
else: return icon
|
else: return icon
|
||||||
_g_EnumHelper_Component: UTIL_functions.EnumPropHelper = UTIL_functions.EnumPropHelper(
|
_g_EnumHelper_Component = UTIL_functions.EnumPropHelper(
|
||||||
PROP_ballance_element.BallanceElementType,
|
PROP_ballance_element.BallanceElementType,
|
||||||
lambda x: str(x.value),
|
lambda x: str(x.value),
|
||||||
lambda x: PROP_ballance_element.BallanceElementType(int(x)),
|
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")
|
layout.prop(self, "component_type")
|
||||||
|
|
||||||
# only show sector for non-PE/PS component
|
# 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:
|
if eletype != PROP_ballance_element.BallanceElementType.PS_FourFlames and eletype != PROP_ballance_element.BallanceElementType.PE_Balloon:
|
||||||
self.draw_component_sector_params(layout)
|
self.draw_component_sector_params(layout)
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import bpy, mathutils, math
|
import bpy, mathutils, math
|
||||||
import typing
|
import typing
|
||||||
from . import UTIL_rail_creator
|
from . import UTIL_rail_creator, PROP_preferences
|
||||||
|
|
||||||
## Const Value Hint:
|
## Const Value Hint:
|
||||||
# Default Rail Radius: 0.35 (in measure)
|
# Default Rail Radius: 0.35 (in measure)
|
||||||
@ -233,6 +233,10 @@ class BBP_OT_add_rail_section(SharedRailSectionInputProperty, bpy.types.Operator
|
|||||||
bl_options = {'REGISTER', 'UNDO'}
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
bl_translation_context = 'BBP_OT_add_rail_section'
|
bl_translation_context = 'BBP_OT_add_rail_section'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder()
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
UTIL_rail_creator.rail_creator_wrapper(
|
UTIL_rail_creator.rail_creator_wrapper(
|
||||||
lambda bm: UTIL_rail_creator.create_rail_section(
|
lambda bm: UTIL_rail_creator.create_rail_section(
|
||||||
@ -254,6 +258,10 @@ class BBP_OT_add_transition_section(bpy.types.Operator):
|
|||||||
bl_options = {'REGISTER', 'UNDO'}
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
bl_translation_context = 'BBP_OT_add_transition_section'
|
bl_translation_context = 'BBP_OT_add_transition_section'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder()
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
UTIL_rail_creator.rail_creator_wrapper(
|
UTIL_rail_creator.rail_creator_wrapper(
|
||||||
lambda bm: UTIL_rail_creator.create_transition_section(bm, c_DefaultRailRadius, c_DefaultRailSpan),
|
lambda bm: UTIL_rail_creator.create_transition_section(bm, c_DefaultRailRadius, c_DefaultRailSpan),
|
||||||
@ -272,6 +280,10 @@ class BBP_OT_add_straight_rail(SharedExtraTransform, SharedRailSectionInputPrope
|
|||||||
bl_options = {'REGISTER', 'UNDO'}
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
bl_translation_context = 'BBP_OT_add_straight_rail'
|
bl_translation_context = 'BBP_OT_add_straight_rail'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder()
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
UTIL_rail_creator.rail_creator_wrapper(
|
UTIL_rail_creator.rail_creator_wrapper(
|
||||||
lambda bm: UTIL_rail_creator.create_straight_rail(
|
lambda bm: UTIL_rail_creator.create_straight_rail(
|
||||||
@ -301,6 +313,10 @@ class BBP_OT_add_transition_rail(SharedExtraTransform, SharedRailCapInputPropert
|
|||||||
bl_options = {'REGISTER', 'UNDO'}
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
bl_translation_context = 'BBP_OT_add_transition_rail'
|
bl_translation_context = 'BBP_OT_add_transition_rail'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder()
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
UTIL_rail_creator.rail_creator_wrapper(
|
UTIL_rail_creator.rail_creator_wrapper(
|
||||||
lambda bm: UTIL_rail_creator.create_transition_rail(
|
lambda bm: UTIL_rail_creator.create_transition_rail(
|
||||||
@ -340,6 +356,10 @@ class BBP_OT_add_side_rail(SharedExtraTransform, SharedRailCapInputProperty, Sha
|
|||||||
translation_context = 'BBP_OT_add_side_rail/property'
|
translation_context = 'BBP_OT_add_side_rail/property'
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder()
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
UTIL_rail_creator.rail_creator_wrapper(
|
UTIL_rail_creator.rail_creator_wrapper(
|
||||||
lambda bm: UTIL_rail_creator.create_straight_rail(
|
lambda bm: UTIL_rail_creator.create_straight_rail(
|
||||||
@ -379,6 +399,10 @@ class BBP_OT_add_arc_rail(SharedExtraTransform, SharedRailSectionInputProperty,
|
|||||||
translation_context = 'BBP_OT_add_arc_rail/property'
|
translation_context = 'BBP_OT_add_arc_rail/property'
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder()
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
UTIL_rail_creator.rail_creator_wrapper(
|
UTIL_rail_creator.rail_creator_wrapper(
|
||||||
lambda bm: UTIL_rail_creator.create_screw_rail(
|
lambda bm: UTIL_rail_creator.create_screw_rail(
|
||||||
@ -430,6 +454,10 @@ class BBP_OT_add_spiral_rail(SharedExtraTransform, SharedRailCapInputProperty, S
|
|||||||
translation_context = 'BBP_OT_add_spiral_rail/property'
|
translation_context = 'BBP_OT_add_spiral_rail/property'
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder()
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
UTIL_rail_creator.rail_creator_wrapper(
|
UTIL_rail_creator.rail_creator_wrapper(
|
||||||
lambda bm: UTIL_rail_creator.create_screw_rail(
|
lambda bm: UTIL_rail_creator.create_screw_rail(
|
||||||
@ -474,6 +502,10 @@ class BBP_OT_add_side_spiral_rail(SharedExtraTransform, SharedRailSectionInputPr
|
|||||||
translation_context = 'BBP_OT_add_side_spiral_rail/property'
|
translation_context = 'BBP_OT_add_side_spiral_rail/property'
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder()
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
UTIL_rail_creator.rail_creator_wrapper(
|
UTIL_rail_creator.rail_creator_wrapper(
|
||||||
lambda bm: UTIL_rail_creator.create_screw_rail(
|
lambda bm: UTIL_rail_creator.create_screw_rail(
|
||||||
|
@ -18,10 +18,16 @@ class BBP_OT_export_virtools(bpy.types.Operator, UTIL_file_browser.ExportVirtool
|
|||||||
return (
|
return (
|
||||||
PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder()
|
PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder()
|
||||||
and bmap.is_bmap_available())
|
and bmap.is_bmap_available())
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
# preset virtools encoding if possible
|
||||||
|
self.preset_vt_encodings_if_possible(context)
|
||||||
|
# call parent invoke function (same reason written in IMPORT module)
|
||||||
|
return super().invoke(context, event)
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
# check selecting first
|
# check selecting first
|
||||||
objls: tuple[bpy.types.Object] | None = self.general_get_export_objects(context)
|
objls: tuple[bpy.types.Object, ...] | None = self.general_get_export_objects(context)
|
||||||
if objls is None:
|
if objls is None:
|
||||||
self.report({'ERROR'}, 'No selected target!')
|
self.report({'ERROR'}, 'No selected target!')
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
@ -38,10 +44,16 @@ class BBP_OT_export_virtools(bpy.types.Operator, UTIL_file_browser.ExportVirtool
|
|||||||
self.report({'ERROR'}, 'You must specify at least one encoding for file saving (e.g. cp1252, gbk)!')
|
self.report({'ERROR'}, 'You must specify at least one encoding for file saving (e.g. cp1252, gbk)!')
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
# check file name
|
||||||
|
filename = self.general_get_filename()
|
||||||
|
if not os.path.isfile(filename):
|
||||||
|
self.report({'ERROR'}, 'No file was selected!')
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
# start exporting
|
# start exporting
|
||||||
with UTIL_ioport_shared.ExportEditModeBackup() as editmode_guard:
|
with UTIL_ioport_shared.ExportEditModeBackup() as editmode_guard:
|
||||||
_export_virtools(
|
_export_virtools(
|
||||||
self.general_get_filename(),
|
filename,
|
||||||
encodings,
|
encodings,
|
||||||
texture_save_opt,
|
texture_save_opt,
|
||||||
self.general_get_use_compress(),
|
self.general_get_use_compress(),
|
||||||
@ -68,7 +80,7 @@ _TTexturePair = tuple[bpy.types.Image, bmap.BMTexture]
|
|||||||
|
|
||||||
def _export_virtools(
|
def _export_virtools(
|
||||||
file_name_: str,
|
file_name_: str,
|
||||||
encodings_: tuple[str],
|
encodings_: tuple[str, ...],
|
||||||
texture_save_opt_: UTIL_virtools_types.CK_TEXTURE_SAVEOPTIONS,
|
texture_save_opt_: UTIL_virtools_types.CK_TEXTURE_SAVEOPTIONS,
|
||||||
use_compress_: bool,
|
use_compress_: bool,
|
||||||
compress_level_: int,
|
compress_level_: int,
|
||||||
|
@ -18,6 +18,12 @@ class BBP_OT_import_virtools(bpy.types.Operator, UTIL_file_browser.ImportVirtool
|
|||||||
return (
|
return (
|
||||||
PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder()
|
PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder()
|
||||||
and bmap.is_bmap_available())
|
and bmap.is_bmap_available())
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
# preset virtools encoding if possible
|
||||||
|
self.preset_vt_encodings_if_possible(context)
|
||||||
|
# call parent invoke function (do no call self "execute", because we need show a modal window)
|
||||||
|
return super().invoke(context, event)
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
# check whether encoding list is empty to avoid real stupid user.
|
# check whether encoding list is empty to avoid real stupid user.
|
||||||
@ -26,8 +32,14 @@ class BBP_OT_import_virtools(bpy.types.Operator, UTIL_file_browser.ImportVirtool
|
|||||||
self.report({'ERROR'}, 'You must specify at least one encoding for file loading (e.g. cp1252, gbk)!')
|
self.report({'ERROR'}, 'You must specify at least one encoding for file loading (e.g. cp1252, gbk)!')
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
# check file name
|
||||||
|
filename = self.general_get_filename()
|
||||||
|
if not os.path.isfile(filename):
|
||||||
|
self.report({'ERROR'}, 'No file was selected!')
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
_import_virtools(
|
_import_virtools(
|
||||||
self.general_get_filename(),
|
filename,
|
||||||
encodings,
|
encodings,
|
||||||
self.general_get_conflict_resolver()
|
self.general_get_conflict_resolver()
|
||||||
)
|
)
|
||||||
@ -40,7 +52,7 @@ class BBP_OT_import_virtools(bpy.types.Operator, UTIL_file_browser.ImportVirtool
|
|||||||
self.draw_virtools_params(context, layout, True)
|
self.draw_virtools_params(context, layout, True)
|
||||||
self.draw_ballance_params(layout, True)
|
self.draw_ballance_params(layout, True)
|
||||||
|
|
||||||
def _import_virtools(file_name_: str, encodings_: tuple[str], resolver: UTIL_ioport_shared.ConflictResolver) -> None:
|
def _import_virtools(file_name_: str, encodings_: tuple[str, ...], resolver: UTIL_ioport_shared.ConflictResolver) -> None:
|
||||||
# create temp folder
|
# create temp folder
|
||||||
with tempfile.TemporaryDirectory() as vt_temp_folder:
|
with tempfile.TemporaryDirectory() as vt_temp_folder:
|
||||||
tr_text: str = bpy.app.translations.pgettext_rpt(
|
tr_text: str = bpy.app.translations.pgettext_rpt(
|
||||||
|
399
bbp_ng/OP_OBJECT_game_view.py
Normal file
399
bbp_ng/OP_OBJECT_game_view.py
Normal file
@ -0,0 +1,399 @@
|
|||||||
|
import bpy, mathutils
|
||||||
|
import typing, enum, math
|
||||||
|
from . import UTIL_functions
|
||||||
|
|
||||||
|
# TODO:
|
||||||
|
# This file should have fully refactor after we finish Virtools Camera import and export,
|
||||||
|
# because this module is highly rely on it. Current implementation is a compromise.
|
||||||
|
# There is a list of things to be done:
|
||||||
|
# - Remove BBP_OT_game_resolution operator, because Virtools Camera will have similar function in panel.
|
||||||
|
# - Update BBP_OT_game_cameraoperator with Virtools Camera.
|
||||||
|
|
||||||
|
#region Game Resolution
|
||||||
|
|
||||||
|
class ResolutionKind(enum.IntEnum):
|
||||||
|
Normal = enum.auto()
|
||||||
|
Extended = enum.auto()
|
||||||
|
Widescreen = enum.auto()
|
||||||
|
Panoramic = enum.auto()
|
||||||
|
|
||||||
|
def to_resolution(self) -> tuple[int, int]:
|
||||||
|
match self:
|
||||||
|
case ResolutionKind.Normal: return (1024, 768)
|
||||||
|
case ResolutionKind.Extended: return (1280, 720)
|
||||||
|
case ResolutionKind.Widescreen: return (1400, 600)
|
||||||
|
case ResolutionKind.Panoramic: return (2000, 700)
|
||||||
|
|
||||||
|
_g_ResolutionKindDesc: dict[ResolutionKind, tuple[str, str]] = {
|
||||||
|
ResolutionKind.Normal: ("Normal", "Aspect ratio: 4:3."),
|
||||||
|
ResolutionKind.Extended: ("Extended", "Aspect ratio: 16:9."),
|
||||||
|
ResolutionKind.Widescreen: ("Widescreen", "Aspect ratio: 7:3."),
|
||||||
|
ResolutionKind.Panoramic: ("Panoramic", "Aspect ratio: 20:7."),
|
||||||
|
}
|
||||||
|
_g_EnumHelper_ResolutionKind = UTIL_functions.EnumPropHelper(
|
||||||
|
ResolutionKind,
|
||||||
|
lambda x: str(x.value),
|
||||||
|
lambda x: ResolutionKind(int(x)),
|
||||||
|
lambda x: _g_ResolutionKindDesc[x][0],
|
||||||
|
lambda x: _g_ResolutionKindDesc[x][1],
|
||||||
|
lambda _: ""
|
||||||
|
)
|
||||||
|
|
||||||
|
class BBP_OT_game_resolution(bpy.types.Operator):
|
||||||
|
"""Set Blender render resolution to Ballance game"""
|
||||||
|
bl_idname = "bbp.game_resolution"
|
||||||
|
bl_label = "Game Resolution"
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
bl_translation_context = 'BBP_OT_game_resolution'
|
||||||
|
|
||||||
|
resolution_kind: bpy.props.EnumProperty(
|
||||||
|
name = "Resolution Kind",
|
||||||
|
description = "The type of preset resolution.",
|
||||||
|
items = _g_EnumHelper_ResolutionKind.generate_items(),
|
||||||
|
default = _g_EnumHelper_ResolutionKind.to_selection(ResolutionKind.Normal)
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
return self.execute(context)
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
layout.use_property_split = True
|
||||||
|
layout.prop(self, 'resolution_kind')
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
# fetch resolution
|
||||||
|
resolution_kind = _g_EnumHelper_ResolutionKind.get_selection(self.resolution_kind)
|
||||||
|
resolution = resolution_kind.to_resolution()
|
||||||
|
# setup resolution
|
||||||
|
render_settings = bpy.context.scene.render
|
||||||
|
render_settings.resolution_x = resolution[0]
|
||||||
|
render_settings.resolution_y = resolution[1]
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Game Camera
|
||||||
|
|
||||||
|
#region Enum Defines
|
||||||
|
|
||||||
|
class TargetKind(enum.IntEnum):
|
||||||
|
Cursor = enum.auto()
|
||||||
|
ActiveObject = enum.auto()
|
||||||
|
_g_TargetKindDesc: dict[TargetKind, tuple[str, str, str]] = {
|
||||||
|
TargetKind.Cursor: ("3D Cursor", "3D cursor is player ball.", "CURSOR"),
|
||||||
|
TargetKind.ActiveObject: ("Active Object", "The origin point of active object is player ball.", "OBJECT_DATA"),
|
||||||
|
}
|
||||||
|
_g_EnumHelper_TargetKind = UTIL_functions.EnumPropHelper(
|
||||||
|
TargetKind,
|
||||||
|
lambda x: str(x.value),
|
||||||
|
lambda x: TargetKind(int(x)),
|
||||||
|
lambda x: _g_TargetKindDesc[x][0],
|
||||||
|
lambda x: _g_TargetKindDesc[x][1],
|
||||||
|
lambda x: _g_TargetKindDesc[x][2],
|
||||||
|
)
|
||||||
|
|
||||||
|
class RotationKind(enum.IntEnum):
|
||||||
|
Preset = enum.auto()
|
||||||
|
Custom = enum.auto()
|
||||||
|
_g_RotationKindDesc: dict[RotationKind, tuple[str, str]] = {
|
||||||
|
RotationKind.Preset: ("Preset", "8 preset rotation angles usually used in game."),
|
||||||
|
RotationKind.Custom: ("Custom", "User manually input rotation angle.")
|
||||||
|
}
|
||||||
|
_g_EnumHelper_RotationKind = UTIL_functions.EnumPropHelper(
|
||||||
|
RotationKind,
|
||||||
|
lambda x: str(x.value),
|
||||||
|
lambda x: RotationKind(int(x)),
|
||||||
|
lambda x: _g_RotationKindDesc[x][0],
|
||||||
|
lambda x: _g_RotationKindDesc[x][1],
|
||||||
|
lambda _: ""
|
||||||
|
)
|
||||||
|
|
||||||
|
class RotationAngle(enum.IntEnum):
|
||||||
|
Deg0 = enum.auto()
|
||||||
|
Deg45 = enum.auto()
|
||||||
|
Deg90 = enum.auto()
|
||||||
|
Deg135 = enum.auto()
|
||||||
|
Deg180 = enum.auto()
|
||||||
|
Deg225 = enum.auto()
|
||||||
|
Deg270 = enum.auto()
|
||||||
|
Deg315 = enum.auto()
|
||||||
|
|
||||||
|
def to_degree(self) -> float:
|
||||||
|
match self:
|
||||||
|
case RotationAngle.Deg0: return 0
|
||||||
|
case RotationAngle.Deg45: return 45
|
||||||
|
case RotationAngle.Deg90: return 90
|
||||||
|
case RotationAngle.Deg135: return 135
|
||||||
|
case RotationAngle.Deg180: return 180
|
||||||
|
case RotationAngle.Deg225: return 225
|
||||||
|
case RotationAngle.Deg270: return 270
|
||||||
|
case RotationAngle.Deg315: return 315
|
||||||
|
|
||||||
|
def to_radians(self) -> float:
|
||||||
|
return math.radians(self.to_degree())
|
||||||
|
|
||||||
|
_g_RotationAngleDesc: dict[RotationAngle, tuple[str, str]] = {
|
||||||
|
# TODO: Add axis direction in description after we add Camera support when importing
|
||||||
|
# (because we only can confirm game camera behavior after that).
|
||||||
|
RotationAngle.Deg0: ("0 Degree", "0 degree"),
|
||||||
|
RotationAngle.Deg45: ("45 Degree", "45 degree"),
|
||||||
|
RotationAngle.Deg90: ("90 Degree", "90 degree"),
|
||||||
|
RotationAngle.Deg135: ("135 Degree", "135 degree"),
|
||||||
|
RotationAngle.Deg180: ("180 Degree", "180 degree"),
|
||||||
|
RotationAngle.Deg225: ("225 Degree", "225 degree"),
|
||||||
|
RotationAngle.Deg270: ("270 Degree", "270 degree"),
|
||||||
|
RotationAngle.Deg315: ("315 Degree", "315 degree"),
|
||||||
|
}
|
||||||
|
_g_EnumHelper_RotationAngle = UTIL_functions.EnumPropHelper(
|
||||||
|
RotationAngle,
|
||||||
|
lambda x: str(x.value),
|
||||||
|
lambda x: RotationAngle(int(x)),
|
||||||
|
lambda x: _g_RotationAngleDesc[x][0],
|
||||||
|
lambda x: _g_RotationAngleDesc[x][1],
|
||||||
|
lambda _: ""
|
||||||
|
)
|
||||||
|
|
||||||
|
class PerspectiveKind(enum.IntEnum):
|
||||||
|
Ordinary = enum.auto()
|
||||||
|
Lift = enum.auto()
|
||||||
|
EasterEgg = enum.auto()
|
||||||
|
_g_PerspectiveKindDesc: dict[PerspectiveKind, tuple[str, str]] = {
|
||||||
|
PerspectiveKind.Ordinary: ("Ordinary", "The default perspective for game camera."),
|
||||||
|
PerspectiveKind.Lift: ("Lift", "Lifted camera in game for downcast level."),
|
||||||
|
PerspectiveKind.EasterEgg: ("Easter Egg", "A very close view to player ball in game."),
|
||||||
|
}
|
||||||
|
_g_EnumHelper_PerspectiveKind = UTIL_functions.EnumPropHelper(
|
||||||
|
PerspectiveKind,
|
||||||
|
lambda x: str(x.value),
|
||||||
|
lambda x: PerspectiveKind(int(x)),
|
||||||
|
lambda x: _g_PerspectiveKindDesc[x][0],
|
||||||
|
lambda x: _g_PerspectiveKindDesc[x][1],
|
||||||
|
lambda _: ""
|
||||||
|
)
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
class BBP_OT_game_camera(bpy.types.Operator):
|
||||||
|
"""Order active camera look at target like Ballance does"""
|
||||||
|
bl_idname = "bbp.game_camera"
|
||||||
|
bl_label = "Game Camera"
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
bl_translation_context = 'BBP_OT_game_camera'
|
||||||
|
|
||||||
|
target_kind: bpy.props.EnumProperty(
|
||||||
|
name = "Target Kind",
|
||||||
|
description = "",
|
||||||
|
items = _g_EnumHelper_TargetKind.generate_items(),
|
||||||
|
default = _g_EnumHelper_TargetKind.to_selection(TargetKind.Cursor)
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
|
rotation_kind: bpy.props.EnumProperty(
|
||||||
|
name = "Rotation Angle Kind",
|
||||||
|
description = "",
|
||||||
|
items = _g_EnumHelper_RotationKind.generate_items(),
|
||||||
|
default = _g_EnumHelper_RotationKind.to_selection(RotationKind.Preset)
|
||||||
|
) # type: ignore
|
||||||
|
preset_rotation_angle: bpy.props.EnumProperty(
|
||||||
|
name = "Preset Rotation Angle",
|
||||||
|
description = "",
|
||||||
|
items = _g_EnumHelper_RotationAngle.generate_items(),
|
||||||
|
default = _g_EnumHelper_RotationAngle.to_selection(RotationAngle.Deg0)
|
||||||
|
) # type: ignore
|
||||||
|
custom_rotation_angle: bpy.props.FloatProperty(
|
||||||
|
name = "Custom Rotation Angle",
|
||||||
|
description = "The rotation angle of camera relative to 3D Cursor",
|
||||||
|
subtype = 'ANGLE',
|
||||||
|
min = 0, max = math.radians(360),
|
||||||
|
step = 100,
|
||||||
|
# MARK: What the fuck of the precision?
|
||||||
|
# I set it to 2 but it doesn't work so I forcely set it to 100.
|
||||||
|
precision = 100,
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
|
perspective_kind: bpy.props.EnumProperty(
|
||||||
|
name = "Rotation Angle Kind",
|
||||||
|
description = "",
|
||||||
|
items = _g_EnumHelper_PerspectiveKind.generate_items(),
|
||||||
|
default = _g_EnumHelper_PerspectiveKind.to_selection(PerspectiveKind.Ordinary)
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
# find camera object
|
||||||
|
camera_obj = _find_camera_obj()
|
||||||
|
if camera_obj is None: return False
|
||||||
|
# find active object
|
||||||
|
active_obj = bpy.context.active_object
|
||||||
|
if active_obj is None: return False
|
||||||
|
# camera object should not be active object
|
||||||
|
return camera_obj != active_obj
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
# order user enter camera view
|
||||||
|
_enter_camera_view()
|
||||||
|
# then execute following code
|
||||||
|
return self.execute(context)
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
|
||||||
|
# Show target picker
|
||||||
|
layout.label(text='Target', text_ctxt='BBP_OT_game_camera/draw')
|
||||||
|
layout.row().prop(self, 'target_kind', expand=True)
|
||||||
|
|
||||||
|
# Show rotation angle according to different types.
|
||||||
|
layout.separator()
|
||||||
|
layout.label(text='Rotation', text_ctxt='BBP_OT_game_camera/draw')
|
||||||
|
layout.row().prop(self, 'rotation_kind', expand=True)
|
||||||
|
rot_kind = _g_EnumHelper_RotationKind.get_selection(self.rotation_kind)
|
||||||
|
match rot_kind:
|
||||||
|
case RotationKind.Preset:
|
||||||
|
layout.prop(self, 'preset_rotation_angle', text='')
|
||||||
|
case RotationKind.Custom:
|
||||||
|
layout.prop(self, 'custom_rotation_angle', text='')
|
||||||
|
|
||||||
|
# Show perspective kind
|
||||||
|
layout.separator()
|
||||||
|
layout.label(text='Perspective', text_ctxt='BBP_OT_game_camera/draw')
|
||||||
|
layout.row().prop(self, 'perspective_kind', expand=True)
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
# fetch angle
|
||||||
|
angle: float
|
||||||
|
rot_kind = _g_EnumHelper_RotationKind.get_selection(self.rotation_kind)
|
||||||
|
match rot_kind:
|
||||||
|
case RotationKind.Preset:
|
||||||
|
rot_angle = _g_EnumHelper_RotationAngle.get_selection(self.preset_rotation_angle)
|
||||||
|
angle = rot_angle.to_radians()
|
||||||
|
case RotationKind.Custom:
|
||||||
|
angle = float(self.custom_rotation_angle)
|
||||||
|
# fetch others
|
||||||
|
camera_obj = typing.cast(bpy.types.Object, _find_camera_obj())
|
||||||
|
target_kind = _g_EnumHelper_TargetKind.get_selection(self.target_kind)
|
||||||
|
perspective_kind = _g_EnumHelper_PerspectiveKind.get_selection(self.perspective_kind)
|
||||||
|
|
||||||
|
# setup its transform and properties
|
||||||
|
glob_trans = _fetch_glob_translation(camera_obj, target_kind)
|
||||||
|
_setup_camera_transform(camera_obj, angle, perspective_kind, glob_trans)
|
||||||
|
_setup_camera_properties(camera_obj)
|
||||||
|
|
||||||
|
# return
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
def _find_3d_view_space() -> bpy.types.SpaceView3D | None:
|
||||||
|
# get current area
|
||||||
|
area = bpy.context.area
|
||||||
|
if area is None: return None
|
||||||
|
|
||||||
|
# check whether it is 3d view
|
||||||
|
if area.type != 'VIEW_3D': return None
|
||||||
|
|
||||||
|
# get the active space in area
|
||||||
|
space = area.spaces.active
|
||||||
|
if space is None: return None
|
||||||
|
|
||||||
|
# okey. cast its type and return
|
||||||
|
return typing.cast(bpy.types.SpaceView3D, space)
|
||||||
|
|
||||||
|
def _enter_camera_view() -> None:
|
||||||
|
space = _find_3d_view_space()
|
||||||
|
if space is None: return
|
||||||
|
|
||||||
|
region = space.region_3d
|
||||||
|
if region is None: return
|
||||||
|
|
||||||
|
region.view_perspective = 'CAMERA'
|
||||||
|
|
||||||
|
def _find_camera_obj() -> bpy.types.Object | None:
|
||||||
|
space = _find_3d_view_space()
|
||||||
|
if space is None: return None
|
||||||
|
|
||||||
|
return space.camera
|
||||||
|
|
||||||
|
def _fetch_glob_translation(camobj: bpy.types.Object, target_kind: TargetKind) -> mathutils.Vector:
|
||||||
|
# we have checked any bad cases in "poll",
|
||||||
|
# so we can simply return value in there without any check.
|
||||||
|
match target_kind:
|
||||||
|
case TargetKind.Cursor:
|
||||||
|
return bpy.context.scene.cursor.location
|
||||||
|
case TargetKind.ActiveObject:
|
||||||
|
return bpy.context.active_object.location
|
||||||
|
|
||||||
|
def _setup_camera_transform(camobj: bpy.types.Object, angle: float, perspective: PerspectiveKind, glob_trans: mathutils.Vector) -> None:
|
||||||
|
# decide the camera offset with ref point
|
||||||
|
ingamecam_pos: mathutils.Vector
|
||||||
|
match perspective:
|
||||||
|
case PerspectiveKind.Ordinary:
|
||||||
|
ingamecam_pos = mathutils.Vector((22, 0, 35))
|
||||||
|
case PerspectiveKind.Lift:
|
||||||
|
ingamecam_pos = mathutils.Vector((22, 0, 35 + 20))
|
||||||
|
case PerspectiveKind.EasterEgg:
|
||||||
|
ingamecam_pos = mathutils.Vector((22, 0, 3.86))
|
||||||
|
|
||||||
|
# decide the position of ref point
|
||||||
|
refpot_pos: mathutils.Vector
|
||||||
|
match perspective:
|
||||||
|
case PerspectiveKind.EasterEgg:
|
||||||
|
refpot_pos = mathutils.Vector((4.4, 0, 0))
|
||||||
|
case _:
|
||||||
|
refpot_pos = mathutils.Vector((0, 0, 0))
|
||||||
|
|
||||||
|
# perform rotation for both positions
|
||||||
|
player_rot_mat = mathutils.Matrix.Rotation(angle, 4, 'Z')
|
||||||
|
ingamecam_pos = ingamecam_pos @ player_rot_mat
|
||||||
|
refpot_pos = refpot_pos @ player_rot_mat
|
||||||
|
|
||||||
|
# calculate the rotation of camera
|
||||||
|
|
||||||
|
# YYC MARK:
|
||||||
|
# Following code are linear algebra required.
|
||||||
|
#
|
||||||
|
# We can calulate the direction of camera by simply substracting 2 vector.
|
||||||
|
# In default, the direction of camera is -Z, up direction is +Y.
|
||||||
|
# So this computed direction is -Z in new cooredinate system.
|
||||||
|
# Now we can compute +Z axis in this new coordinate system.
|
||||||
|
new_z = (ingamecam_pos - refpot_pos)
|
||||||
|
new_z.normalize()
|
||||||
|
# For ballance camera, all camera is +Z up.
|
||||||
|
# So we can use it to compute +X axis in new coordinate system
|
||||||
|
assistant_y = mathutils.Vector((0, 0, 1))
|
||||||
|
new_x = typing.cast(mathutils.Vector, assistant_y.cross(new_z))
|
||||||
|
new_x.normalize()
|
||||||
|
# now we calc the final axis
|
||||||
|
new_y = typing.cast(mathutils.Vector, new_z.cross(new_x))
|
||||||
|
new_y.normalize()
|
||||||
|
# okey, we conbine them as a matrix
|
||||||
|
rot_mat = mathutils.Matrix((
|
||||||
|
(new_x.x, new_y.x, new_z.x, 0),
|
||||||
|
(new_x.y, new_y.y, new_z.y, 0),
|
||||||
|
(new_x.z, new_y.z, new_z.z, 0),
|
||||||
|
(0, 0, 0, 1)
|
||||||
|
))
|
||||||
|
|
||||||
|
# calc the final transform matrix and apply it
|
||||||
|
trans_mat = mathutils.Matrix.Translation(ingamecam_pos)
|
||||||
|
glob_trans_mat = mathutils.Matrix.Translation(glob_trans)
|
||||||
|
camobj.matrix_world = glob_trans_mat @ trans_mat @ rot_mat
|
||||||
|
|
||||||
|
def _setup_camera_properties(camobj: bpy.types.Object) -> None:
|
||||||
|
# fetch camera
|
||||||
|
camera = typing.cast(bpy.types.Camera, camobj.data)
|
||||||
|
|
||||||
|
# set clipping
|
||||||
|
camera.clip_start = 4
|
||||||
|
camera.clip_end = 1200
|
||||||
|
# set FOV
|
||||||
|
camera.lens_unit = 'FOV'
|
||||||
|
camera.angle = math.radians(58)
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
def register() -> None:
|
||||||
|
bpy.utils.register_class(BBP_OT_game_resolution)
|
||||||
|
bpy.utils.register_class(BBP_OT_game_camera)
|
||||||
|
|
||||||
|
def unregister() -> None:
|
||||||
|
bpy.utils.unregister_class(BBP_OT_game_camera)
|
||||||
|
bpy.utils.unregister_class(BBP_OT_game_resolution)
|
||||||
|
|
@ -9,19 +9,35 @@ class AlignMode(enum.IntEnum):
|
|||||||
BBoxCenter = enum.auto()
|
BBoxCenter = enum.auto()
|
||||||
AxisCenter = enum.auto()
|
AxisCenter = enum.auto()
|
||||||
Max = enum.auto()
|
Max = enum.auto()
|
||||||
_g_AlignModeDesc: dict[AlignMode, tuple[str, str]] = {
|
_g_AlignModeDesc: dict[AlignMode, tuple[str, str, str]] = {
|
||||||
AlignMode.Min: ("Min", "The min value in specified axis."),
|
AlignMode.Min: ("Min", "The min value in specified axis.", "REMOVE"),
|
||||||
AlignMode.BBoxCenter: ("Center (Bounding Box)", "The bounding box center in specified axis."),
|
AlignMode.BBoxCenter: ("Center (Bounding Box)", "The bounding box center in specified axis.", "SHADING_BBOX"),
|
||||||
AlignMode.AxisCenter: ("Center (Axis)", "The object's source point in specified axis."),
|
AlignMode.AxisCenter: ("Center (Axis)", "The object's source point in specified axis.", "OBJECT_ORIGIN"),
|
||||||
AlignMode.Max: ("Max", "The max value in specified axis."),
|
AlignMode.Max: ("Max", "The max value in specified axis.", "ADD"),
|
||||||
}
|
}
|
||||||
_g_EnumHelper_AlignMode: UTIL_functions.EnumPropHelper = UTIL_functions.EnumPropHelper(
|
_g_EnumHelper_AlignMode = UTIL_functions.EnumPropHelper(
|
||||||
AlignMode,
|
AlignMode,
|
||||||
lambda x: str(x.value),
|
lambda x: str(x.value),
|
||||||
lambda x: AlignMode(int(x)),
|
lambda x: AlignMode(int(x)),
|
||||||
lambda x: _g_AlignModeDesc[x][0],
|
lambda x: _g_AlignModeDesc[x][0],
|
||||||
lambda x: _g_AlignModeDesc[x][1],
|
lambda x: _g_AlignModeDesc[x][1],
|
||||||
lambda _: ''
|
lambda x: _g_AlignModeDesc[x][2]
|
||||||
|
)
|
||||||
|
|
||||||
|
class CurrentInstance(enum.IntEnum):
|
||||||
|
ActiveObject = enum.auto()
|
||||||
|
Cursor = enum.auto()
|
||||||
|
_g_CurrentInstanceDesc: dict[CurrentInstance, tuple[str, str, str]] = {
|
||||||
|
CurrentInstance.ActiveObject: ("Active Object", "Use Active Object as Current Object", "OBJECT_DATA"),
|
||||||
|
CurrentInstance.Cursor: ("3D Cursor", "Use 3D Cursor as Current Object", "CURSOR"),
|
||||||
|
}
|
||||||
|
_g_EnumHelper_CurrentInstance = UTIL_functions.EnumPropHelper(
|
||||||
|
CurrentInstance,
|
||||||
|
lambda x: str(x.value),
|
||||||
|
lambda x: CurrentInstance(int(x)),
|
||||||
|
lambda x: _g_CurrentInstanceDesc[x][0],
|
||||||
|
lambda x: _g_CurrentInstanceDesc[x][1],
|
||||||
|
lambda x: _g_CurrentInstanceDesc[x][2]
|
||||||
)
|
)
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@ -55,14 +71,23 @@ class BBP_PG_legacy_align_history(bpy.types.PropertyGroup):
|
|||||||
default = False,
|
default = False,
|
||||||
translation_context = 'BBP_PG_legacy_align_history/property'
|
translation_context = 'BBP_PG_legacy_align_history/property'
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
|
current_instance: bpy.props.EnumProperty(
|
||||||
|
name = "Current Instance",
|
||||||
|
description = "Decide which instance should be used as Current Object",
|
||||||
|
items = _g_EnumHelper_CurrentInstance.generate_items(),
|
||||||
|
default = _g_EnumHelper_CurrentInstance.to_selection(CurrentInstance.ActiveObject),
|
||||||
|
translation_context = 'BBP_PG_legacy_align_history/property'
|
||||||
|
) # type: ignore
|
||||||
current_align_mode: bpy.props.EnumProperty(
|
current_align_mode: bpy.props.EnumProperty(
|
||||||
name = "Current Object (Active Object)",
|
name = "Current Object",
|
||||||
|
description = "The align mode applied to Current Object",
|
||||||
items = _g_EnumHelper_AlignMode.generate_items(),
|
items = _g_EnumHelper_AlignMode.generate_items(),
|
||||||
default = _g_EnumHelper_AlignMode.to_selection(AlignMode.AxisCenter),
|
default = _g_EnumHelper_AlignMode.to_selection(AlignMode.AxisCenter),
|
||||||
translation_context = 'BBP_PG_legacy_align_history/property'
|
translation_context = 'BBP_PG_legacy_align_history/property'
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
target_align_mode: bpy.props.EnumProperty(
|
target_align_mode: bpy.props.EnumProperty(
|
||||||
name = "Target Objects (Selected Objects)",
|
name = "Target Objects",
|
||||||
|
description = "The align mode applied to Target Objects (selected objects except active object if Current Instance is active object)",
|
||||||
items = _g_EnumHelper_AlignMode.generate_items(),
|
items = _g_EnumHelper_AlignMode.generate_items(),
|
||||||
default = _g_EnumHelper_AlignMode.to_selection(AlignMode.AxisCenter),
|
default = _g_EnumHelper_AlignMode.to_selection(AlignMode.AxisCenter),
|
||||||
translation_context = 'BBP_PG_legacy_align_history/property'
|
translation_context = 'BBP_PG_legacy_align_history/property'
|
||||||
@ -148,7 +173,7 @@ class BBP_OT_legacy_align(bpy.types.Operator):
|
|||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
# get processed objects
|
# get processed objects
|
||||||
(current_obj, target_objs) = _prepare_objects()
|
(current_obj, current_cursor, target_objs) = _prepare_objects()
|
||||||
# YYC MARK:
|
# YYC MARK:
|
||||||
# This statement is VERY IMPORTANT.
|
# This statement is VERY IMPORTANT.
|
||||||
# If this statement is not presented, Blender will return identity matrix
|
# If this statement is not presented, Blender will return identity matrix
|
||||||
@ -162,7 +187,8 @@ class BBP_OT_legacy_align(bpy.types.Operator):
|
|||||||
histories = UTIL_functions.CollectionVisitor(self.align_history)
|
histories = UTIL_functions.CollectionVisitor(self.align_history)
|
||||||
for entry in histories:
|
for entry in histories:
|
||||||
_align_objects(
|
_align_objects(
|
||||||
current_obj, target_objs,
|
_g_EnumHelper_CurrentInstance.get_selection(entry.current_instance),
|
||||||
|
current_obj, current_cursor, target_objs,
|
||||||
entry.align_x, entry.align_y, entry.align_z,
|
entry.align_x, entry.align_y, entry.align_z,
|
||||||
_g_EnumHelper_AlignMode.get_selection(entry.current_align_mode),
|
_g_EnumHelper_AlignMode.get_selection(entry.current_align_mode),
|
||||||
_g_EnumHelper_AlignMode.get_selection(entry.target_align_mode)
|
_g_EnumHelper_AlignMode.get_selection(entry.target_align_mode)
|
||||||
@ -185,11 +211,22 @@ class BBP_OT_legacy_align(bpy.types.Operator):
|
|||||||
row.prop(entry, "align_y", toggle = 1)
|
row.prop(entry, "align_y", toggle = 1)
|
||||||
row.prop(entry, "align_z", toggle = 1)
|
row.prop(entry, "align_z", toggle = 1)
|
||||||
|
|
||||||
# show mode
|
# show current instance
|
||||||
col.separator()
|
col.separator()
|
||||||
col.label(text='Current Object (Active Object)', text_ctxt='BBP_OT_legacy_align/draw')
|
col.label(text='Current Object', text_ctxt='BBP_OT_legacy_align/draw')
|
||||||
col.prop(entry, "current_align_mode", expand = True)
|
# it should be shown in horizon so we create a new sublayout
|
||||||
col.label(text='Target Objects (Selected Objects)', text_ctxt='BBP_OT_legacy_align/draw')
|
row = col.row()
|
||||||
|
row.prop(entry, 'current_instance', expand=True)
|
||||||
|
|
||||||
|
# show instance and mode
|
||||||
|
col.separator()
|
||||||
|
# only show current object mode if current instance is active object,
|
||||||
|
# because there is no mode for 3d cursor.
|
||||||
|
current_instnce = _g_EnumHelper_CurrentInstance.get_selection(entry.current_instance)
|
||||||
|
if current_instnce == CurrentInstance.ActiveObject:
|
||||||
|
col.label(text='Current Object Align Mode', text_ctxt='BBP_OT_legacy_align/draw')
|
||||||
|
col.prop(entry, "current_align_mode", expand = True)
|
||||||
|
col.label(text='Target Objects Align Mode', text_ctxt='BBP_OT_legacy_align/draw')
|
||||||
col.prop(entry, "target_align_mode", expand = True)
|
col.prop(entry, "target_align_mode", expand = True)
|
||||||
|
|
||||||
# show apply button
|
# show apply button
|
||||||
@ -206,44 +243,66 @@ class BBP_OT_legacy_align(bpy.types.Operator):
|
|||||||
#region Core Functions
|
#region Core Functions
|
||||||
|
|
||||||
def _check_align_requirement() -> bool:
|
def _check_align_requirement() -> bool:
|
||||||
# if we are not in object mode, do not do legacy align
|
# If we are not in object mode, do not do legacy align
|
||||||
if not UTIL_functions.is_in_object_mode():
|
if not UTIL_functions.is_in_object_mode():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# check current obj
|
# YYC MARK:
|
||||||
|
# We still need to check active object (as current object)
|
||||||
|
# although we can choose align with active object or 3d cursor.
|
||||||
|
# Because we can not make any promise that user will
|
||||||
|
# select Active Object or 3D Cursor as current object before executing this operator.
|
||||||
if bpy.context.active_object is None:
|
if bpy.context.active_object is None:
|
||||||
return False
|
return False
|
||||||
# check target obj with filter of current obj
|
|
||||||
length = len(bpy.context.selected_objects)
|
# YYC MARK:
|
||||||
if bpy.context.active_object in bpy.context.selected_objects:
|
# Roughly check selected objects.
|
||||||
length -= 1
|
# We do not need exclude active object from selected objects,
|
||||||
return length != 0
|
# because active object may be moved when 3D Cursor is current object.
|
||||||
|
if len(bpy.context.selected_objects) == 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
def _prepare_objects() -> tuple[bpy.types.Object, set[bpy.types.Object]]:
|
def _prepare_objects() -> tuple[bpy.types.Object, mathutils.Vector, list[bpy.types.Object]]:
|
||||||
# get current object
|
# Fetch current object
|
||||||
current_obj: bpy.types.Object = bpy.context.active_object
|
current_obj = typing.cast(bpy.types.Object, bpy.context.active_object)
|
||||||
|
|
||||||
# get target objects
|
# Fetch 3d cursor location
|
||||||
target_objs: set[bpy.types.Object] = set(bpy.context.selected_objects)
|
current_cursor: mathutils.Vector = bpy.context.scene.cursor.location
|
||||||
# remove active one
|
|
||||||
if current_obj in target_objs:
|
# YYC MARK:
|
||||||
target_objs.remove(current_obj)
|
# Fetch target objects and do NOT remove active object from it.
|
||||||
|
# because active object will be moved when current instance is 3D Cursor.
|
||||||
|
target_objs: list[bpy.types.Object] = bpy.context.selected_objects[:]
|
||||||
|
|
||||||
# return value
|
# return value
|
||||||
return (current_obj, target_objs)
|
return (current_obj, current_cursor, target_objs)
|
||||||
|
|
||||||
def _align_objects(
|
def _align_objects(
|
||||||
current_obj: bpy.types.Object, target_objs: set[bpy.types.Object],
|
current_instance: CurrentInstance,
|
||||||
|
current_obj: bpy.types.Object, current_cursor: mathutils.Vector, target_objs: list[bpy.types.Object],
|
||||||
align_x: bool, align_y: bool, align_z: bool, current_mode: AlignMode, target_mode: AlignMode) -> None:
|
align_x: bool, align_y: bool, align_z: bool, current_mode: AlignMode, target_mode: AlignMode) -> None:
|
||||||
# if no align, skip
|
# if no align, skip
|
||||||
if not (align_x or align_y or align_z):
|
if not (align_x or align_y or align_z):
|
||||||
return
|
return
|
||||||
|
|
||||||
# calc current object data
|
# calc current object data
|
||||||
current_obj_ref: mathutils.Vector = _get_object_ref_point(current_obj, current_mode)
|
current_obj_ref: mathutils.Vector
|
||||||
|
match current_instance:
|
||||||
|
case CurrentInstance.ActiveObject:
|
||||||
|
current_obj_ref = _get_object_ref_point(current_obj, current_mode)
|
||||||
|
case CurrentInstance.Cursor:
|
||||||
|
current_obj_ref = current_cursor
|
||||||
|
|
||||||
# process each target obj
|
# process each target obj
|
||||||
for target_obj in target_objs:
|
for target_obj in target_objs:
|
||||||
|
# YYC MARK:
|
||||||
|
# If we use active object as current instance, we need exclude it from target objects,
|
||||||
|
# because there is no pre-exclude considering the scenario that 3D Cursor is current instance.
|
||||||
|
if current_instance == CurrentInstance.ActiveObject and current_obj == target_obj:
|
||||||
|
continue
|
||||||
|
|
||||||
# calc target object data
|
# calc target object data
|
||||||
target_obj_ref: mathutils.Vector = _get_object_ref_point(target_obj, target_mode)
|
target_obj_ref: mathutils.Vector = _get_object_ref_point(target_obj, target_mode)
|
||||||
# build translation transform
|
# build translation transform
|
||||||
@ -256,21 +315,21 @@ def _align_objects(
|
|||||||
# apply translation transform to left side (add into original matrix)
|
# apply translation transform to left side (add into original matrix)
|
||||||
target_obj.matrix_world = target_obj_translation_matrix @ target_obj.matrix_world
|
target_obj.matrix_world = target_obj_translation_matrix @ target_obj.matrix_world
|
||||||
|
|
||||||
bpy.context.scene.update_tag
|
|
||||||
|
|
||||||
def _get_object_ref_point(obj: bpy.types.Object, mode: AlignMode) -> mathutils.Vector:
|
def _get_object_ref_point(obj: bpy.types.Object, mode: AlignMode) -> mathutils.Vector:
|
||||||
ref_pos: mathutils.Vector = mathutils.Vector((0, 0, 0))
|
ref_pos = mathutils.Vector((0, 0, 0))
|
||||||
|
|
||||||
# calc bounding box data
|
# calc bounding box data
|
||||||
corners: tuple[mathutils.Vector] = tuple(obj.matrix_world @ mathutils.Vector(corner) for corner in obj.bound_box)
|
corners: tuple[mathutils.Vector, ...] = tuple(obj.matrix_world @ mathutils.Vector(corner) for corner in obj.bound_box)
|
||||||
bbox_min_corner: mathutils.Vector = mathutils.Vector((0, 0, 0))
|
bbox_min_corner = mathutils.Vector((
|
||||||
bbox_min_corner.x = min((vec.x for vec in corners))
|
min((vec.x for vec in corners)),
|
||||||
bbox_min_corner.y = min((vec.y for vec in corners))
|
min((vec.y for vec in corners)),
|
||||||
bbox_min_corner.z = min((vec.z for vec in corners))
|
min((vec.z for vec in corners)),
|
||||||
bbox_max_corner: mathutils.Vector = mathutils.Vector((0, 0, 0))
|
))
|
||||||
bbox_max_corner.x = max((vec.x for vec in corners))
|
bbox_max_corner = mathutils.Vector((
|
||||||
bbox_max_corner.y = max((vec.y for vec in corners))
|
max((vec.x for vec in corners)),
|
||||||
bbox_max_corner.z = max((vec.z for vec in corners))
|
max((vec.y for vec in corners)),
|
||||||
|
max((vec.z for vec in corners)),
|
||||||
|
))
|
||||||
|
|
||||||
# return value by given align mode
|
# return value by given align mode
|
||||||
match(mode):
|
match(mode):
|
||||||
|
@ -18,7 +18,7 @@ _g_SelectModeDesc: dict[SelectMode, tuple[str, str, str]] = {
|
|||||||
SelectMode.Difference: ('Invert', 'Inverts the selection.', 'SELECT_DIFFERENCE'),
|
SelectMode.Difference: ('Invert', 'Inverts the selection.', 'SELECT_DIFFERENCE'),
|
||||||
SelectMode.Intersect: ('Intersect', 'Selects items that intersect with the existing selection.', 'SELECT_INTERSECT')
|
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,
|
SelectMode,
|
||||||
lambda x: str(x.value),
|
lambda x: str(x.value),
|
||||||
lambda x: SelectMode(int(x)),
|
lambda x: SelectMode(int(x)),
|
||||||
|
@ -195,6 +195,16 @@ class PropsVisitor():
|
|||||||
def get_ioport_encodings(self) -> tuple[str, ...]:
|
def get_ioport_encodings(self) -> tuple[str, ...]:
|
||||||
encodings = get_ioport_encodings(self.__mAssocScene)
|
encodings = get_ioport_encodings(self.__mAssocScene)
|
||||||
return tuple(i.encoding for i in encodings)
|
return tuple(i.encoding for i in encodings)
|
||||||
|
def preset_ioport_encodings(self) -> None:
|
||||||
|
"""
|
||||||
|
Set IOPort used encodings list as preset encoding list.
|
||||||
|
Please note that all old values will be overwritten.
|
||||||
|
"""
|
||||||
|
encodings = get_ioport_encodings(self.__mAssocScene)
|
||||||
|
encodings.clear()
|
||||||
|
for default_enc in UTIL_virtools_types.g_PyBMapDefaultEncodings:
|
||||||
|
item = encodings.add()
|
||||||
|
item.encoding = default_enc
|
||||||
def draw_ioport_encodings(self, layout: bpy.types.UILayout) -> None:
|
def draw_ioport_encodings(self, layout: bpy.types.UILayout) -> None:
|
||||||
target = get_ptrprop_resolver(self.__mAssocScene)
|
target = get_ptrprop_resolver(self.__mAssocScene)
|
||||||
row = layout.row()
|
row = layout.row()
|
||||||
@ -218,24 +228,11 @@ class PropsVisitor():
|
|||||||
col.separator()
|
col.separator()
|
||||||
col.operator(BBP_OT_clear_ioport_encodings.bl_idname, icon='TRASH', text='')
|
col.operator(BBP_OT_clear_ioport_encodings.bl_idname, icon='TRASH', text='')
|
||||||
|
|
||||||
@bpy.app.handlers.persistent
|
|
||||||
def _ioport_encodings_initializer(file_path: str):
|
|
||||||
# if we can fetch property, and it is empty after loading file
|
|
||||||
# we fill it with default value
|
|
||||||
encodings = get_ioport_encodings(bpy.context.scene)
|
|
||||||
if len(encodings) == 0:
|
|
||||||
for default_enc in UTIL_virtools_types.g_PyBMapDefaultEncodings:
|
|
||||||
item = encodings.add()
|
|
||||||
item.encoding = default_enc
|
|
||||||
|
|
||||||
def register() -> None:
|
def register() -> None:
|
||||||
bpy.utils.register_class(BBP_PG_bmap_encoding)
|
bpy.utils.register_class(BBP_PG_bmap_encoding)
|
||||||
bpy.utils.register_class(BBP_UL_bmap_encoding)
|
bpy.utils.register_class(BBP_UL_bmap_encoding)
|
||||||
bpy.utils.register_class(BBP_PG_ptrprop_resolver)
|
bpy.utils.register_class(BBP_PG_ptrprop_resolver)
|
||||||
|
|
||||||
# register ioport encodings default value
|
|
||||||
bpy.app.handlers.load_post.append(_ioport_encodings_initializer)
|
|
||||||
|
|
||||||
bpy.utils.register_class(BBP_OT_add_ioport_encodings)
|
bpy.utils.register_class(BBP_OT_add_ioport_encodings)
|
||||||
bpy.utils.register_class(BBP_OT_rm_ioport_encodings)
|
bpy.utils.register_class(BBP_OT_rm_ioport_encodings)
|
||||||
bpy.utils.register_class(BBP_OT_up_ioport_encodings)
|
bpy.utils.register_class(BBP_OT_up_ioport_encodings)
|
||||||
@ -253,9 +250,6 @@ def unregister() -> None:
|
|||||||
bpy.utils.unregister_class(BBP_OT_rm_ioport_encodings)
|
bpy.utils.unregister_class(BBP_OT_rm_ioport_encodings)
|
||||||
bpy.utils.unregister_class(BBP_OT_add_ioport_encodings)
|
bpy.utils.unregister_class(BBP_OT_add_ioport_encodings)
|
||||||
|
|
||||||
# unregister ioport encodings default value
|
|
||||||
bpy.app.handlers.load_post.remove(_ioport_encodings_initializer)
|
|
||||||
|
|
||||||
bpy.utils.unregister_class(BBP_PG_ptrprop_resolver)
|
bpy.utils.unregister_class(BBP_PG_ptrprop_resolver)
|
||||||
bpy.utils.unregister_class(BBP_UL_bmap_encoding)
|
bpy.utils.unregister_class(BBP_UL_bmap_encoding)
|
||||||
bpy.utils.unregister_class(BBP_PG_bmap_encoding)
|
bpy.utils.unregister_class(BBP_PG_bmap_encoding)
|
||||||
|
@ -205,7 +205,7 @@ class VirtoolsGroupsPreset(enum.Enum):
|
|||||||
|
|
||||||
Shadow = "Shadow"
|
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
|
## Some of group names are not matched with icon name
|
||||||
# So we create a convertion map to convert them.
|
# 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
|
if value is not None: return value
|
||||||
else: return UTIL_icons_manager.get_empty_icon()
|
else: return UTIL_icons_manager.get_empty_icon()
|
||||||
# blender group name prop helper
|
# blender group name prop helper
|
||||||
_g_EnumHelper_Group: UTIL_functions.EnumPropHelper = UTIL_functions.EnumPropHelper(
|
_g_EnumHelper_Group = UTIL_functions.EnumPropHelper(
|
||||||
VirtoolsGroupsPreset,
|
VirtoolsGroupsPreset,
|
||||||
lambda x: x.value, # member is string self
|
lambda x: x.value, # member is string self
|
||||||
lambda x: VirtoolsGroupsPreset(x), # convert directly because it is StrEnum.
|
lambda x: VirtoolsGroupsPreset(x), # convert directly because it is StrEnum.
|
||||||
|
@ -73,7 +73,7 @@ class RawVirtoolsLight():
|
|||||||
|
|
||||||
# Blender Property Group
|
# 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):
|
class BBP_PG_virtools_light(bpy.types.PropertyGroup):
|
||||||
light_type: bpy.props.EnumProperty(
|
light_type: bpy.props.EnumProperty(
|
||||||
|
@ -114,13 +114,13 @@ class RawVirtoolsMaterial():
|
|||||||
|
|
||||||
#region Blender Enum Prop Helper (Virtools type specified)
|
#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_BLENDMODE = 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_FILTERMODE = 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_VXTEXTURE_ADDRESSMODE = 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_VXBLEND_MODE = 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_VXFILL_MODE = 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_VXSHADE_MODE = 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_VXCMPFUNC = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VXCMPFUNC)
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@ -558,7 +558,7 @@ def preset_virtools_material(mtl: bpy.types.Material, preset_type: MaterialPrese
|
|||||||
set_raw_virtools_material(mtl, preset_data)
|
set_raw_virtools_material(mtl, preset_data)
|
||||||
|
|
||||||
# create preset enum blender helper
|
# create preset enum blender helper
|
||||||
_g_Helper_MtlPreset: UTIL_functions.EnumPropHelper = UTIL_functions.EnumPropHelper(
|
_g_Helper_MtlPreset = UTIL_functions.EnumPropHelper(
|
||||||
MaterialPresetType,
|
MaterialPresetType,
|
||||||
lambda x: str(x.value),
|
lambda x: str(x.value),
|
||||||
lambda x: MaterialPresetType(int(x)),
|
lambda x: MaterialPresetType(int(x)),
|
||||||
@ -572,13 +572,13 @@ _g_Helper_MtlPreset: UTIL_functions.EnumPropHelper = UTIL_functions.EnumPropHelp
|
|||||||
#region Fix Material
|
#region Fix Material
|
||||||
|
|
||||||
def fix_material(mtl: bpy.types.Material) -> bool:
|
def fix_material(mtl: bpy.types.Material) -> bool:
|
||||||
"""!
|
"""
|
||||||
Fix single Blender material.
|
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.
|
:param mtl: The blender material need to be processed.
|
||||||
@return True if we do a fix, otherwise return False.
|
:return: True if we do a fix, otherwise return False.
|
||||||
"""
|
"""
|
||||||
# prepare return value first
|
# prepare return value first
|
||||||
ret: bool = False
|
ret: bool = False
|
||||||
|
@ -15,7 +15,7 @@ class RawVirtoolsMesh():
|
|||||||
self.mLitMode = kwargs.get('mLitMode', RawVirtoolsMesh.cDefaultLitMode)
|
self.mLitMode = kwargs.get('mLitMode', RawVirtoolsMesh.cDefaultLitMode)
|
||||||
|
|
||||||
# blender enum prop helper defines
|
# 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
|
# Blender Property Group
|
||||||
|
|
||||||
|
@ -20,8 +20,8 @@ class RawVirtoolsTexture():
|
|||||||
self.mVideoFormat = kwargs.get('mVideoFormat', RawVirtoolsTexture.cDefaultVideoFormat)
|
self.mVideoFormat = kwargs.get('mVideoFormat', RawVirtoolsTexture.cDefaultVideoFormat)
|
||||||
|
|
||||||
# blender enum prop helper defines
|
# 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_CK_TEXTURE_SAVEOPTIONS = 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_VX_PIXELFORMAT = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.VX_PIXELFORMAT)
|
||||||
|
|
||||||
class BBP_PG_virtools_texture(bpy.types.PropertyGroup):
|
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
|
#region Virtools Texture Drawer
|
||||||
|
|
||||||
"""!
|
# YYC MARK:
|
||||||
@remark
|
# Because Image do not have its unique properties window,
|
||||||
Because Image do not have its unique properties window
|
# so we only can draw Virtools Texture properties in other window.
|
||||||
so we only can draw virtools texture properties in other window
|
# We provide various functions to help draw properties.
|
||||||
we provide various function to help draw property.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def draw_virtools_texture(img: bpy.types.Image, layout: bpy.types.UILayout):
|
def draw_virtools_texture(img: bpy.types.Image, layout: bpy.types.UILayout):
|
||||||
props: BBP_PG_virtools_texture = get_virtools_texture(img)
|
props: BBP_PG_virtools_texture = get_virtools_texture(img)
|
||||||
|
@ -24,6 +24,7 @@ TOKEN_IDENTIFIER: str = 'identifier'
|
|||||||
|
|
||||||
TOKEN_SHOWCASE: str = 'showcase'
|
TOKEN_SHOWCASE: str = 'showcase'
|
||||||
TOKEN_SHOWCASE_TITLE: str = 'title'
|
TOKEN_SHOWCASE_TITLE: str = 'title'
|
||||||
|
TOKEN_SHOWCASE_CATEGORY: str = 'category'
|
||||||
TOKEN_SHOWCASE_ICON: str = 'icon'
|
TOKEN_SHOWCASE_ICON: str = 'icon'
|
||||||
TOKEN_SHOWCASE_TYPE: str = 'type'
|
TOKEN_SHOWCASE_TYPE: str = 'type'
|
||||||
TOKEN_SHOWCASE_CFGS: str = 'cfgs'
|
TOKEN_SHOWCASE_CFGS: str = 'cfgs'
|
||||||
@ -64,10 +65,10 @@ TOKEN_INSTANCES_TRANSFORM: str = 'transform'
|
|||||||
|
|
||||||
#region Prototype Loader
|
#region Prototype Loader
|
||||||
|
|
||||||
## The list storing BME prototype.
|
|
||||||
_g_BMEPrototypes: list[dict[str, typing.Any]] = []
|
_g_BMEPrototypes: list[dict[str, typing.Any]] = []
|
||||||
## The dict. Key is prototype identifier. value is the index of prototype in prototype list.
|
"""The list storing BME prototype."""
|
||||||
_g_BMEPrototypeIndexMap: dict[str, int] = {}
|
_g_BMEPrototypeIndexMap: dict[str, int] = {}
|
||||||
|
"""The dict. Key is prototype identifier. Value is the index of prototype in prototype list."""
|
||||||
|
|
||||||
# the core loader
|
# the core loader
|
||||||
for walk_root, walk_dirs, walk_files in os.walk(os.path.join(os.path.dirname(__file__), 'jsons')):
|
for walk_root, walk_dirs, walk_files in os.walk(os.path.join(os.path.dirname(__file__), 'jsons')):
|
||||||
@ -99,7 +100,7 @@ def _env_fct_angle(x1: float, y1: float, x2: float, y2: float) -> float:
|
|||||||
# second, its direction (clockwise is positive) is opposite with blender rotation direction (counter-clockwise is positive).
|
# second, its direction (clockwise is positive) is opposite with blender rotation direction (counter-clockwise is positive).
|
||||||
diff = mathutils.Vector((x2, y2)) - mathutils.Vector((x1, y1))
|
diff = mathutils.Vector((x2, y2)) - mathutils.Vector((x1, y1))
|
||||||
bld_angle = math.degrees(mathutils.Vector((1,0)).angle_signed(diff, 0))
|
bld_angle = math.degrees(mathutils.Vector((1,0)).angle_signed(diff, 0))
|
||||||
|
|
||||||
# flip it first
|
# flip it first
|
||||||
bld_angle = -bld_angle
|
bld_angle = -bld_angle
|
||||||
# process positove number and negative number respectively
|
# process positove number and negative number respectively
|
||||||
@ -141,7 +142,7 @@ _g_ProgFieldGlobals: dict[str, typing.Any] = {
|
|||||||
'rot': lambda x, y, z: mathutils.Matrix.LocRotScale(None, mathutils.Euler((math.radians(x), math.radians(y), math.radians(z)), 'XYZ'), None),
|
'rot': lambda x, y, z: mathutils.Matrix.LocRotScale(None, mathutils.Euler((math.radians(x), math.radians(y), math.radians(z)), 'XYZ'), None),
|
||||||
'scale': lambda x, y, z: mathutils.Matrix.LocRotScale(None, None, (x, y, z)),
|
'scale': lambda x, y, z: mathutils.Matrix.LocRotScale(None, None, (x, y, z)),
|
||||||
'ident': lambda: mathutils.Matrix.Identity(4),
|
'ident': lambda: mathutils.Matrix.Identity(4),
|
||||||
|
|
||||||
# my misc custom functions
|
# my misc custom functions
|
||||||
'distance': _env_fct_distance,
|
'distance': _env_fct_distance,
|
||||||
'angle': _env_fct_angle,
|
'angle': _env_fct_angle,
|
||||||
@ -187,15 +188,38 @@ class PrototypeShowcaseCfgDescriptor():
|
|||||||
def get_default(self) -> typing.Any:
|
def get_default(self) -> typing.Any:
|
||||||
return _eval_showcase_cfgs_default(self.__mRawCfg[TOKEN_SHOWCASE_CFGS_DEFAULT])
|
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.
|
The BME specialized Blender EnumProperty helper.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
showcase_identifiers: tuple[str, ...]
|
||||||
|
showcase_categories: dict[str, tuple[str, ...]]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
# build cache for showcase identifiers and categories
|
||||||
|
# prepare cache value
|
||||||
|
identifiers: list[str] = []
|
||||||
|
categories: dict[str, list[str]] = {}
|
||||||
|
# iterate showcase prototypes
|
||||||
|
for x in filter(lambda x: x[TOKEN_SHOWCASE] is not None, _g_BMEPrototypes):
|
||||||
|
# fetch identifier and category
|
||||||
|
identifier = typing.cast(str, x[TOKEN_IDENTIFIER])
|
||||||
|
category = typing.cast(str, x[TOKEN_SHOWCASE][TOKEN_SHOWCASE_CATEGORY])
|
||||||
|
# add into identifier list
|
||||||
|
identifiers.append(identifier)
|
||||||
|
# add into categories
|
||||||
|
categories_inner = categories.get(category, None)
|
||||||
|
if categories_inner is None:
|
||||||
|
categories_inner = []
|
||||||
|
categories[category] = categories_inner
|
||||||
|
categories_inner.append(identifier)
|
||||||
|
# tuple the result
|
||||||
|
self.showcase_identifiers = tuple(identifiers)
|
||||||
|
self.showcase_categories = {k: tuple(v) for k, v in categories.items()}
|
||||||
|
|
||||||
# init parent class
|
# init parent class
|
||||||
UTIL_functions.EnumPropHelper.__init__(
|
super().__init__(
|
||||||
self,
|
|
||||||
self.get_bme_identifiers(),
|
self.get_bme_identifiers(),
|
||||||
lambda x: x,
|
lambda x: x,
|
||||||
lambda x: x,
|
lambda x: x,
|
||||||
@ -203,17 +227,20 @@ class EnumPropHelper(UTIL_functions.EnumPropHelper):
|
|||||||
lambda _: '',
|
lambda _: '',
|
||||||
lambda x: self.get_bme_showcase_icon(x)
|
lambda x: self.get_bme_showcase_icon(x)
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_bme_identifiers(self) -> tuple[str, ...]:
|
def get_bme_identifiers(self) -> tuple[str, ...]:
|
||||||
"""
|
"""
|
||||||
Get the identifier of prototype which need to be exposed to user.
|
Get the identifier of prototype which need to be exposed to user.
|
||||||
Template prototype is not included.
|
In other words, template prototype is not included.
|
||||||
"""
|
"""
|
||||||
return tuple(
|
return self.showcase_identifiers
|
||||||
x[TOKEN_IDENTIFIER] # get identifier
|
|
||||||
for x in filter(lambda x: x[TOKEN_SHOWCASE] is not None, _g_BMEPrototypes) # filter() to filter no showcase template.
|
|
||||||
)
|
|
||||||
|
|
||||||
|
def get_bme_categories(self) -> dict[str, tuple[str, ...]]:
|
||||||
|
"""
|
||||||
|
Get user-oriented identifier list grouped by category.
|
||||||
|
"""
|
||||||
|
return self.showcase_categories
|
||||||
|
|
||||||
def get_bme_showcase_title(self, ident: str) -> str:
|
def get_bme_showcase_title(self, ident: str) -> str:
|
||||||
"""
|
"""
|
||||||
Get BME display title by prototype identifier.
|
Get BME display title by prototype identifier.
|
||||||
@ -327,14 +354,14 @@ def create_bme_struct(
|
|||||||
# create mtl slot remap to help following mesh adding
|
# create mtl slot remap to help following mesh adding
|
||||||
# because mesh writer do not accept string format mtl slot visiting,
|
# because mesh writer do not accept string format mtl slot visiting,
|
||||||
# it only accept int based mtl slot index.
|
# it only accept int based mtl slot index.
|
||||||
#
|
#
|
||||||
# Also we build face used mtl slot index at the same time.
|
# Also we build face used mtl slot index at the same time.
|
||||||
# So we do not analyse texture field again when providing face data.
|
# So we do not analyse texture field again when providing face data.
|
||||||
# The result is in `prebuild_face_mtl_idx` and please note it will store all face's mtl index.
|
# The result is in `prebuild_face_mtl_idx` and please note it will store all face's mtl index.
|
||||||
# For example: if face 0 is skipped and face 1 is used, the first entry in `prebuild_face_mtl_idx`
|
# For example: if face 0 is skipped and face 1 is used, the first entry in `prebuild_face_mtl_idx`
|
||||||
# will be the mtl slot index used by face 0, not 1. And its length is equal to the face count.
|
# will be the mtl slot index used by face 0, not 1. And its length is equal to the face count.
|
||||||
# However, because face 0 is skipped, so the entry is not used and default set to 0.
|
# However, because face 0 is skipped, so the entry is not used and default set to 0.
|
||||||
#
|
#
|
||||||
# NOTE: since Python 3.6, the item of builtin dict is ordered by inserting order.
|
# NOTE: since Python 3.6, the item of builtin dict is ordered by inserting order.
|
||||||
# we rely on this to implement following features.
|
# we rely on this to implement following features.
|
||||||
mtl_remap: dict[str, int] = {}
|
mtl_remap: dict[str, int] = {}
|
||||||
@ -352,7 +379,7 @@ def create_bme_struct(
|
|||||||
# if existing, no need to add into remap
|
# if existing, no need to add into remap
|
||||||
# but we need get its index from remap
|
# but we need get its index from remap
|
||||||
prebuild_face_mtl_idx[face_idx] = mtl_remap.get(mtl_name, 0)
|
prebuild_face_mtl_idx[face_idx] = mtl_remap.get(mtl_name, 0)
|
||||||
|
|
||||||
# pre-compute vertices data because we may need used later.
|
# pre-compute vertices data because we may need used later.
|
||||||
# Because if face normal data is null, it mean that we need to compute it
|
# Because if face normal data is null, it mean that we need to compute it
|
||||||
# by given vertices.
|
# by given vertices.
|
||||||
@ -367,7 +394,7 @@ def create_bme_struct(
|
|||||||
cache_bv = typing.cast(mathutils.Vector, transform @ cache_bv)
|
cache_bv = typing.cast(mathutils.Vector, transform @ cache_bv)
|
||||||
# get result
|
# get result
|
||||||
prebuild_vec_data.append((cache_bv.x, cache_bv.y, cache_bv.z))
|
prebuild_vec_data.append((cache_bv.x, cache_bv.y, cache_bv.z))
|
||||||
|
|
||||||
# Check whether given transform is mirror matrix
|
# Check whether given transform is mirror matrix
|
||||||
# because mirror matrix will reverse triangle indice order.
|
# because mirror matrix will reverse triangle indice order.
|
||||||
# If matrix is mirror matrix, we need reverse it again in following procession,
|
# If matrix is mirror matrix, we need reverse it again in following procession,
|
||||||
|
@ -2,37 +2,35 @@ import bpy, mathutils
|
|||||||
import math, typing, enum, sys
|
import math, typing, enum, sys
|
||||||
|
|
||||||
class BBPException(Exception):
|
class BBPException(Exception):
|
||||||
"""
|
""" The exception thrown by Ballance Blender Plugin"""
|
||||||
The exception thrown by Ballance Blender Plugin
|
|
||||||
"""
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def clamp_float(v: float, min_val: float, max_val: float) -> float:
|
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 v: The value need to be clamp.
|
||||||
@param min_val[in] The allowed minium value, including self.
|
:param min_val: The allowed minium value (inclusive).
|
||||||
@param max_val[in] The allowed maxium value, including self.
|
:param max_val: The allowed maxium value (inclusive).
|
||||||
@return Clamped value.
|
:return: Clamped value.
|
||||||
"""
|
"""
|
||||||
if (max_val < min_val): raise BBPException("Invalid range of clamp_float().")
|
if (max_val < min_val): raise BBPException("Invalid range of clamp_float().")
|
||||||
|
|
||||||
if (v < min_val): return min_val
|
if (v < min_val): return min_val
|
||||||
elif (v > max_val): return max_val
|
elif (v > max_val): return max_val
|
||||||
else: return v
|
else: return v
|
||||||
|
|
||||||
def clamp_int(v: int, min_val: int, max_val: int) -> int:
|
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 v: The value need to be clamp.
|
||||||
@param min_val[in] The allowed minium value, including self.
|
:param min_val: The allowed minium value (inclusive).
|
||||||
@param max_val[in] The allowed maxium value, including self.
|
:param max_val: The allowed maxium value (inclusive).
|
||||||
@return Clamped value.
|
:return: Clamped value.
|
||||||
"""
|
"""
|
||||||
if (max_val < min_val): raise BBPException("Invalid range of clamp_int().")
|
if (max_val < min_val): raise BBPException("Invalid range of clamp_int().")
|
||||||
|
|
||||||
if (v < min_val): return min_val
|
if (v < min_val): return min_val
|
||||||
elif (v > max_val): return max_val
|
elif (v > max_val): return max_val
|
||||||
else: return v
|
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.
|
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 message: 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 title: Message box title text.
|
||||||
@param icon[in] The icon this message box displayed.
|
:param icon: The icon this message box displayed.
|
||||||
"""
|
"""
|
||||||
def draw(self, context: bpy.types.Context):
|
def draw(self, context: bpy.types.Context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
for item in message:
|
for item in message:
|
||||||
layout.label(text=item, translate=False)
|
layout.label(text=item, translate=False)
|
||||||
|
|
||||||
bpy.context.window_manager.popup_menu(draw, title = title, icon = icon)
|
bpy.context.window_manager.popup_menu(draw, title = title, icon = icon)
|
||||||
|
|
||||||
def add_into_scene(obj: bpy.types.Object):
|
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
|
view_layer = bpy.context.view_layer
|
||||||
collection = view_layer.active_layer_collection.collection
|
collection = view_layer.active_layer_collection.collection
|
||||||
collection.objects.link(obj)
|
collection.objects.link(obj)
|
||||||
|
|
||||||
def move_to_cursor(obj: bpy.types.Object):
|
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
|
Move given object to the position of cursor.
|
||||||
# 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.
|
|
||||||
|
|
||||||
|
: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.location = bpy.context.scene.cursor.location
|
||||||
obj.matrix_world = obj.matrix_world @ mathutils.Matrix.Translation(bpy.context.scene.cursor.location - obj.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):
|
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)
|
add_into_scene(obj)
|
||||||
move_to_cursor(obj)
|
move_to_cursor(obj)
|
||||||
|
|
||||||
def select_certain_objects(objs: tuple[bpy.types.Object, ...]) -> None:
|
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
|
# deselect all objects first
|
||||||
bpy.ops.object.select_all(action = 'DESELECT')
|
bpy.ops.object.select_all(action = 'DESELECT')
|
||||||
# if no objects, return
|
# if no objects, return
|
||||||
if len(objs) == 0: return
|
if len(objs) == 0: return
|
||||||
|
|
||||||
# set selection for each object
|
# set selection for each object
|
||||||
for obj in objs:
|
for obj in objs:
|
||||||
obj.select_set(True)
|
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]
|
bpy.context.view_layer.objects.active = objs[0]
|
||||||
|
|
||||||
def is_in_object_mode() -> bool:
|
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
|
# get active object from context
|
||||||
obj = bpy.context.active_object
|
obj = bpy.context.active_object
|
||||||
|
|
||||||
# if there is no active object, we think it is in object mode
|
# if there is no active object, we think it is in object mode
|
||||||
if obj is None: return True
|
if obj is None: return True
|
||||||
|
|
||||||
# simply check active object mode
|
# simply check active object mode
|
||||||
return obj.mode == 'OBJECT'
|
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`,
|
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.
|
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
|
__mCollections: typing.Iterable[_TRawEnum]
|
||||||
_TFctToStr = typing.Callable[[typing.Any], str]
|
__mFctToStr: _TFctToStr[_TRawEnum]
|
||||||
_TFctFromStr = typing.Callable[[str], typing.Any]
|
__mFctFromStr: _TFctFromStr[_TRawEnum]
|
||||||
_TFctName = typing.Callable[[typing.Any], str]
|
__mFctName: _TFctName[_TRawEnum]
|
||||||
_TFctDesc = typing.Callable[[typing.Any], str]
|
__mFctDesc: _TFctDesc[_TRawEnum]
|
||||||
_TFctIcon = typing.Callable[[typing.Any], str | int]
|
__mFctIcon: _TFctIcon[_TRawEnum]
|
||||||
|
|
||||||
# define class member
|
def __init__(self, collections: typing.Iterable[_TRawEnum],
|
||||||
|
fct_to_str: _TFctToStr[_TRawEnum], fct_from_str: _TFctFromStr[_TRawEnum],
|
||||||
__mCollections: typing.Iterable[typing.Any]
|
fct_name: _TFctName[_TRawEnum], fct_desc: _TFctDesc[_TRawEnum],
|
||||||
__mFctToStr: _TFctToStr
|
fct_icon: _TFctIcon[_TRawEnum]):
|
||||||
__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):
|
|
||||||
"""
|
"""
|
||||||
Initialize a EnumProperty helper.
|
Initialize an EnumProperty helper.
|
||||||
|
|
||||||
@param collections_ [in] The collection all available enum property entries contained.
|
:param collections: The collection containing all available enum property entries.
|
||||||
It can be enum.Enum or a simple list/tuple/dict.
|
It can be `enum.Enum` or a simple list/tuple.
|
||||||
@param fct_to_str [in] A function pointer converting data collection member to its string format.
|
:param fct_to_str: A function pointer converting data collection member to its string format.
|
||||||
For enum.IntEnum, it can be simply `lambda x: str(x.value)`
|
You must make sure that each members built name is unique in collection!
|
||||||
@param fct_from_str [in] A function pointer getting data collection member from its string format.
|
For `enum.IntEnum`, it can be simple `lambda x: str(x.value)`
|
||||||
For enum.IntEnum, it can be simply `lambda x: TEnum(int(x))`
|
:param fct_from_str: A function pointer getting data collection member from its string format.
|
||||||
@param fct_name [in] A function pointer converting data collection member to its display name.
|
This class promise that given string must can be parsed.
|
||||||
@param fct_desc [in] Same as `fct_name` but return description instead. Return empty string, not None if no description.
|
For `enum.IntEnum`, it can be simple `lambda x: TEnum(int(x))`
|
||||||
@param fct_icon [in] Same as `fct_name` but return the used icon instead. Return empty string if no icon.
|
: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
|
# assign member
|
||||||
self.__mCollections = collections_
|
self.__mCollections = collections
|
||||||
self.__mFctToStr = fct_to_str
|
self.__mFctToStr = fct_to_str
|
||||||
self.__mFctFromStr = fct_from_str
|
self.__mFctFromStr = fct_from_str
|
||||||
self.__mFctName = fct_name
|
self.__mFctName = fct_name
|
||||||
self.__mFctDesc = fct_desc
|
self.__mFctDesc = fct_desc
|
||||||
self.__mFctIcon = fct_icon
|
self.__mFctIcon = fct_icon
|
||||||
|
|
||||||
def generate_items(self) -> tuple[tuple[str, str, str, int | str, int], ...]:
|
def generate_items(self) -> tuple[tuple[str, str, str, int | str, int], ...]:
|
||||||
"""
|
"""
|
||||||
Generate a tuple which can be applied to Blender EnumProperty's "items".
|
Generate a tuple which can be applied to Blender EnumProperty's "items".
|
||||||
@ -152,27 +187,29 @@ class EnumPropHelper():
|
|||||||
return tuple(
|
return tuple(
|
||||||
(
|
(
|
||||||
self.__mFctToStr(member), # call to_str as its token.
|
self.__mFctToStr(member), # call to_str as its token.
|
||||||
self.__mFctName(member),
|
self.__mFctName(member),
|
||||||
self.__mFctDesc(member),
|
self.__mFctDesc(member),
|
||||||
self.__mFctIcon(member),
|
self.__mFctIcon(member),
|
||||||
idx # use hardcode index, not the collection member self.
|
idx # use hardcode index, not the collection member self.
|
||||||
) for idx, member in enumerate(self.__mCollections)
|
) 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.
|
Return collection member from given Blender EnumProp string data.
|
||||||
"""
|
"""
|
||||||
# call from_str fct ptr
|
# call from_str fct ptr
|
||||||
return self.__mFctFromStr(prop)
|
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.
|
Parse collection member to Blender EnumProp acceptable string format.
|
||||||
"""
|
"""
|
||||||
# call to_str fct ptr
|
# call to_str fct ptr
|
||||||
return self.__mFctToStr(val)
|
return self.__mFctToStr(val)
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region Blender Collection Visitor
|
#region Blender Collection Visitor
|
||||||
|
|
||||||
_TPropertyGroup = typing.TypeVar('_TPropertyGroup', bound = bpy.types.PropertyGroup)
|
_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.
|
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.
|
So I create a wrapper for my personal use to reduce type hint errors raised by my linter.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__mSrcProp: bpy.types.CollectionProperty
|
__mSrcProp: bpy.types.CollectionProperty
|
||||||
|
|
||||||
def __init__(self, src_prop: bpy.types.CollectionProperty):
|
def __init__(self, src_prop: bpy.types.CollectionProperty):
|
||||||
self.__mSrcProp = src_prop
|
self.__mSrcProp = src_prop
|
||||||
|
|
||||||
def add(self) -> _TPropertyGroup:
|
def add(self) -> _TPropertyGroup:
|
||||||
"""!
|
"""
|
||||||
@brief Adds a new item to the collection.
|
Adds a new item to the collection.
|
||||||
@return The instance of newly created item.
|
|
||||||
|
:return: The instance of newly created item.
|
||||||
"""
|
"""
|
||||||
return self.__mSrcProp.add()
|
return self.__mSrcProp.add()
|
||||||
|
|
||||||
def remove(self, index: int) -> None:
|
def remove(self, index: int) -> None:
|
||||||
"""!
|
"""
|
||||||
@brief Removes the item at the specified index from the collection.
|
Removes the item at the specified index from the collection.
|
||||||
@param[in] index The index of the item to remove.
|
|
||||||
|
:param index: The index of the item to remove.
|
||||||
"""
|
"""
|
||||||
self.__mSrcProp.remove(index)
|
self.__mSrcProp.remove(index)
|
||||||
|
|
||||||
def move(self, from_index: int, to_index: int) -> None:
|
def move(self, from_index: int, to_index: int) -> None:
|
||||||
"""!
|
"""
|
||||||
@brief Moves an item from one index to another within the collection.
|
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.
|
: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)
|
self.__mSrcProp.move(from_index, to_index)
|
||||||
|
|
||||||
def clear(self) -> None:
|
def clear(self) -> None:
|
||||||
"""!
|
"""
|
||||||
@brief Clears all items from the collection.
|
Clears all items from the collection.
|
||||||
"""
|
"""
|
||||||
self.__mSrcProp.clear()
|
self.__mSrcProp.clear()
|
||||||
|
|
||||||
def __len__(self) -> int:
|
def __len__(self) -> int:
|
||||||
return self.__mSrcProp.__len__()
|
return self.__mSrcProp.__len__()
|
||||||
def __getitem__(self, index: int | str) -> _TPropertyGroup:
|
def __getitem__(self, index: int | str) -> _TPropertyGroup:
|
||||||
@ -238,32 +278,50 @@ _TMutexObject = typing.TypeVar('_TMutexObject')
|
|||||||
|
|
||||||
class TinyMutex(typing.Generic[_TMutexObject]):
|
class TinyMutex(typing.Generic[_TMutexObject]):
|
||||||
"""
|
"""
|
||||||
In this plugin, some class have "with" context feature.
|
In this plugin, some classes have "with" context feature.
|
||||||
However, it is essential to block any futher visiting if some "with" context are operating on some object.
|
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.
|
This is the reason why this tiny mutex is designed.
|
||||||
|
|
||||||
Please note this class is not a real MUTEX.
|
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.
|
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.
|
So it doesn't matter that we do not use lock before operating something.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__mProtectedObjects: set[_TMutexObject]
|
__mProtectedObjects: set[_TMutexObject]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.__mProtectedObjects = set()
|
self.__mProtectedObjects = set()
|
||||||
|
|
||||||
def lock(self, obj: _TMutexObject) -> None:
|
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:
|
if obj in self.__mProtectedObjects:
|
||||||
raise BBPException('It is not allowed that operate multiple "with" contexts on a single object.')
|
raise BBPException('It is not allowed that operate multiple "with" contexts on a single object.')
|
||||||
self.__mProtectedObjects.add(obj)
|
self.__mProtectedObjects.add(obj)
|
||||||
|
|
||||||
def try_lock(self, obj: _TMutexObject) -> bool:
|
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:
|
if obj in self.__mProtectedObjects:
|
||||||
return False
|
return False
|
||||||
self.__mProtectedObjects.add(obj)
|
self.__mProtectedObjects.add(obj)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def unlock(self, obj: _TMutexObject) -> None:
|
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:
|
if obj not in self.__mProtectedObjects:
|
||||||
raise BBPException('It is not allowed that unlock an non-existent object.')
|
raise BBPException('It is not allowed that unlock an non-existent object.')
|
||||||
self.__mProtectedObjects.remove(obj)
|
self.__mProtectedObjects.remove(obj)
|
||||||
|
@ -3,11 +3,13 @@ import enum, typing
|
|||||||
from . import UTIL_virtools_types, UTIL_functions
|
from . import UTIL_virtools_types, UTIL_functions
|
||||||
from . import PROP_ptrprop_resolver, PROP_ballance_map_info
|
from . import PROP_ptrprop_resolver, PROP_ballance_map_info
|
||||||
|
|
||||||
## Intent
|
# INTENT:
|
||||||
# Some importer or exporter may share same properties.
|
# Some importer or exporter may share same properties.
|
||||||
# So we create some shared class and user just need inherit them
|
# So we create some shared class and user just need inherit them
|
||||||
# and call general getter to get user selected data.
|
# and call general getter to get user selected data.
|
||||||
# Also provide draw function thus caller do not need draw the params themselves.
|
# Also provide draw function thus caller do not need draw the params themselves.
|
||||||
|
|
||||||
|
#region Import Params
|
||||||
|
|
||||||
class ConflictStrategy(enum.IntEnum):
|
class ConflictStrategy(enum.IntEnum):
|
||||||
Rename = enum.auto()
|
Rename = enum.auto()
|
||||||
@ -16,7 +18,7 @@ _g_ConflictStrategyDesc: dict[ConflictStrategy, tuple[str, str]] = {
|
|||||||
ConflictStrategy.Rename: ('Rename', 'Rename the new one'),
|
ConflictStrategy.Rename: ('Rename', 'Rename the new one'),
|
||||||
ConflictStrategy.Current: ('Use Current', 'Use current one'),
|
ConflictStrategy.Current: ('Use Current', 'Use current one'),
|
||||||
}
|
}
|
||||||
_g_EnumHelper_ConflictStrategy: UTIL_functions.EnumPropHelper = UTIL_functions.EnumPropHelper(
|
_g_EnumHelper_ConflictStrategy = UTIL_functions.EnumPropHelper(
|
||||||
ConflictStrategy,
|
ConflictStrategy,
|
||||||
lambda x: str(x.value),
|
lambda x: str(x.value),
|
||||||
lambda x: ConflictStrategy(int(x)),
|
lambda x: ConflictStrategy(int(x)),
|
||||||
@ -25,39 +27,6 @@ _g_EnumHelper_ConflictStrategy: UTIL_functions.EnumPropHelper = UTIL_functions.E
|
|||||||
lambda _: ''
|
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():
|
class ConflictResolver():
|
||||||
"""
|
"""
|
||||||
This class frequently used when importing objects.
|
This class frequently used when importing objects.
|
||||||
@ -151,8 +120,6 @@ class ConflictResolver():
|
|||||||
tex.name = name
|
tex.name = name
|
||||||
return (tex, True)
|
return (tex, True)
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
class ImportParams():
|
class ImportParams():
|
||||||
texture_conflict_strategy: bpy.props.EnumProperty(
|
texture_conflict_strategy: bpy.props.EnumProperty(
|
||||||
name = "Texture Name Conflict",
|
name = "Texture Name Conflict",
|
||||||
@ -239,13 +206,65 @@ class ImportParams():
|
|||||||
self.general_get_texture_conflict_strategy()
|
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()
|
||||||
|
_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'),
|
||||||
|
}
|
||||||
|
_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():
|
class ExportParams():
|
||||||
export_mode: bpy.props.EnumProperty(
|
export_mode: bpy.props.EnumProperty(
|
||||||
name = "Export Mode",
|
name = "Export Mode",
|
||||||
items = (
|
description = "Define which 3D objects should be exported",
|
||||||
('COLLECTION', "Collection", "Export a collection", 'OUTLINER_COLLECTION', 0),
|
items = _g_EnumHelper_ExportMode.generate_items(),
|
||||||
('OBJECT', "Object", "Export an object", 'OBJECT_DATA', 1),
|
default = _g_EnumHelper_ExportMode.to_selection(ExportMode.BldColl),
|
||||||
),
|
|
||||||
translation_context = 'BBP/UTIL_ioport_shared.ExportParams/property'
|
translation_context = 'BBP/UTIL_ioport_shared.ExportParams/property'
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
|
|
||||||
@ -262,29 +281,40 @@ class ExportParams():
|
|||||||
horizon_body.prop(self, "export_mode", expand=True)
|
horizon_body.prop(self, "export_mode", expand=True)
|
||||||
|
|
||||||
# draw picker
|
# draw picker
|
||||||
|
export_mode = _g_EnumHelper_ExportMode.get_selection(self.export_mode)
|
||||||
ptrprops = PROP_ptrprop_resolver.PropsVisitor(context.scene)
|
ptrprops = PROP_ptrprop_resolver.PropsVisitor(context.scene)
|
||||||
if self.export_mode == 'COLLECTION':
|
match export_mode:
|
||||||
ptrprops.draw_export_collection(body)
|
case ExportMode.BldColl:
|
||||||
elif self.export_mode == 'OBJECT':
|
ptrprops.draw_export_collection(body)
|
||||||
ptrprops.draw_export_object(body)
|
case ExportMode.BldObj:
|
||||||
|
ptrprops.draw_export_object(body)
|
||||||
|
case ExportMode.BldSelObjs:
|
||||||
|
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.
|
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)
|
ptrprops = PROP_ptrprop_resolver.PropsVisitor(context.scene)
|
||||||
if self.export_mode == 'COLLECTION':
|
match export_mode:
|
||||||
col: bpy.types.Collection = ptrprops.get_export_collection()
|
case ExportMode.BldColl:
|
||||||
if col is None: return None
|
col: bpy.types.Collection = ptrprops.get_export_collection()
|
||||||
else:
|
if col is None: return None
|
||||||
return tuple(col.all_objects)
|
else: return tuple(col.all_objects)
|
||||||
else:
|
case ExportMode.BldObj:
|
||||||
obj: bpy.types.Object = ptrprops.get_export_object()
|
obj: bpy.types.Object = ptrprops.get_export_object()
|
||||||
if obj is None: return None
|
if obj is None: return None
|
||||||
else: return (obj, )
|
else: return (obj, )
|
||||||
|
case ExportMode.BldSelObjs:
|
||||||
|
return tuple(context.selected_objects)
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Virtools Params
|
||||||
|
|
||||||
# define global tex save opt blender enum prop helper
|
# 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():
|
class VirtoolsParams():
|
||||||
texture_save_opt: bpy.props.EnumProperty(
|
texture_save_opt: bpy.props.EnumProperty(
|
||||||
@ -310,6 +340,14 @@ class VirtoolsParams():
|
|||||||
translation_context = 'BBP/UTIL_ioport_shared.VirtoolsParams/property'
|
translation_context = 'BBP/UTIL_ioport_shared.VirtoolsParams/property'
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
|
|
||||||
|
def preset_vt_encodings_if_possible(self, context: bpy.types.Context):
|
||||||
|
"""
|
||||||
|
Set preset value for Virtools Encoding list if there is no value inside it.
|
||||||
|
"""
|
||||||
|
ptrprops = PROP_ptrprop_resolver.PropsVisitor(context.scene)
|
||||||
|
if len(ptrprops.get_ioport_encodings()) == 0:
|
||||||
|
ptrprops.preset_ioport_encodings()
|
||||||
|
|
||||||
def draw_virtools_params(self, context: bpy.types.Context, layout: bpy.types.UILayout, is_importer: bool) -> None:
|
def draw_virtools_params(self, context: bpy.types.Context, layout: bpy.types.UILayout, is_importer: bool) -> None:
|
||||||
header: bpy.types.UILayout
|
header: bpy.types.UILayout
|
||||||
body: bpy.types.UILayout
|
body: bpy.types.UILayout
|
||||||
@ -334,7 +372,6 @@ class VirtoolsParams():
|
|||||||
if self.use_compress:
|
if self.use_compress:
|
||||||
body.prop(self, 'compress_level')
|
body.prop(self, 'compress_level')
|
||||||
|
|
||||||
|
|
||||||
def general_get_vt_encodings(self, context: bpy.types.Context) -> tuple[str, ...]:
|
def general_get_vt_encodings(self, context: bpy.types.Context) -> tuple[str, ...]:
|
||||||
# get from ptrprop resolver then filter empty item
|
# get from ptrprop resolver then filter empty item
|
||||||
ptrprops = PROP_ptrprop_resolver.PropsVisitor(context.scene)
|
ptrprops = PROP_ptrprop_resolver.PropsVisitor(context.scene)
|
||||||
@ -349,6 +386,10 @@ class VirtoolsParams():
|
|||||||
def general_get_compress_level(self) -> int:
|
def general_get_compress_level(self) -> int:
|
||||||
return self.compress_level
|
return self.compress_level
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Ballance Params
|
||||||
|
|
||||||
class BallanceParams():
|
class BallanceParams():
|
||||||
successive_sector: bpy.props.BoolProperty(
|
successive_sector: bpy.props.BoolProperty(
|
||||||
name="Successive Sector",
|
name="Successive Sector",
|
||||||
@ -387,3 +428,5 @@ class BallanceParams():
|
|||||||
map_info: PROP_ballance_map_info.RawBallanceMapInfo
|
map_info: PROP_ballance_map_info.RawBallanceMapInfo
|
||||||
map_info = PROP_ballance_map_info.get_raw_ballance_map_info(bpy.context.scene)
|
map_info = PROP_ballance_map_info.get_raw_ballance_map_info(bpy.context.scene)
|
||||||
return map_info.mSectorCount
|
return map_info.mSectorCount
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
@ -55,14 +55,22 @@ import bpy
|
|||||||
CTX_BBP: str = 'BBP'
|
CTX_BBP: str = 'BBP'
|
||||||
|
|
||||||
# The universal translation context prefix for BME module in BBP_NG plugin.
|
# The universal translation context prefix for BME module in BBP_NG plugin.
|
||||||
CTX_BBP_BME: str = CTX_BBP + '/BME'
|
CTX_BBP_BME: str = f'{CTX_BBP}/BME'
|
||||||
def build_prototype_showcase_context(identifier: str) -> str:
|
CTX_BBP_BME_CATEGORY: str = f'{CTX_BBP_BME}/Category'
|
||||||
|
CTX_BBP_BME_PROTOTYPE: str = f'{CTX_BBP_BME}/Proto'
|
||||||
|
def build_prototype_showcase_category_context() -> str:
|
||||||
|
"""
|
||||||
|
Build the context for getting the translation for BME prototype showcase category.
|
||||||
|
@return The context for getting translation.
|
||||||
|
"""
|
||||||
|
return CTX_BBP_BME_CATEGORY
|
||||||
|
def build_prototype_showcase_title_context(identifier: str) -> str:
|
||||||
"""
|
"""
|
||||||
Build the context for getting the translation for BME prototype showcase title.
|
Build the context for getting the translation for BME prototype showcase title.
|
||||||
@param[in] identifier The identifier of this prototype.
|
@param[in] identifier The identifier of this prototype.
|
||||||
@return The context for getting translation.
|
@return The context for getting translation.
|
||||||
"""
|
"""
|
||||||
return CTX_BBP_BME + '/' + identifier
|
return f'{CTX_BBP_BME_PROTOTYPE}/{identifier}'
|
||||||
def build_prototype_showcase_cfg_context(identifier: str, cfg_index: int) -> str:
|
def build_prototype_showcase_cfg_context(identifier: str, cfg_index: int) -> str:
|
||||||
"""
|
"""
|
||||||
Build the context for getting the translation for BME prototype showcase configuration title or description.
|
Build the context for getting the translation for BME prototype showcase configuration title or description.
|
||||||
@ -70,7 +78,7 @@ def build_prototype_showcase_cfg_context(identifier: str, cfg_index: int) -> str
|
|||||||
@param[in] cfg_index The index of this configuration in this prototype showcase.
|
@param[in] cfg_index The index of this configuration in this prototype showcase.
|
||||||
@return The context for getting translation.
|
@return The context for getting translation.
|
||||||
"""
|
"""
|
||||||
return CTX_BBP_BME + f'/{identifier}/[{cfg_index}]'
|
return f'{CTX_BBP_BME_PROTOTYPE}/{identifier}/[{cfg_index}]'
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
@ -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.
|
Virtools type specified Blender EnumProp helper.
|
||||||
"""
|
"""
|
||||||
__mAnnotationDict: dict[int, EnumAnnotation]
|
__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
|
# set enum type and annotation ref first
|
||||||
self.__mEnumTy = ty
|
self.__mEnumTy = ty
|
||||||
self.__mAnnotationDict = _g_Annotation[ty]
|
self.__mAnnotationDict = _g_Annotation[ty]
|
||||||
# init parent data
|
|
||||||
UTIL_functions.EnumPropHelper.__init__(
|
# YYC MARK:
|
||||||
self,
|
# It seems that Pylance has bad generic analyse ability in there.
|
||||||
self.__mEnumTy, # enum.Enum it self is iterable
|
# 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: 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.__mEnumTy(int(x)), # use stored enum type and int() to get enum member
|
||||||
lambda x: self.__mAnnotationDict[x.value].mDisplayName,
|
lambda x: self.__mAnnotationDict[x.value].mDisplayName,
|
||||||
@ -265,11 +272,11 @@ def virtools_name_regulator(name: str | None) -> str:
|
|||||||
if name: return name
|
if name: return name
|
||||||
else: return bpy.app.translations.pgettext_data('annoymous', 'BME/UTIL_virtools_types.virtools_name_regulator()')
|
else: return bpy.app.translations.pgettext_data('annoymous', 'BME/UTIL_virtools_types.virtools_name_regulator()')
|
||||||
|
|
||||||
## Default Encoding for PyBMap
|
# YYC MARK:
|
||||||
# Use semicolon split each encodings. Support Western European and Simplified Chinese in default.
|
# 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.
|
# 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.
|
# So no need set it according to different platform.
|
||||||
# Use universal encoding name (like Python).
|
# Use universal encoding name (like Python).
|
||||||
g_PyBMapDefaultEncodings: tuple[str, ...] = (
|
g_PyBMapDefaultEncodings: tuple[str, ...] = (
|
||||||
'cp1252',
|
'cp1252',
|
||||||
'gbk'
|
'gbk'
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
#region Reload and Import
|
#region Import and Reload
|
||||||
|
|
||||||
# import core lib
|
# import core lib
|
||||||
import bpy
|
import bpy
|
||||||
import typing, collections
|
import typing, enum
|
||||||
|
|
||||||
# reload if needed
|
# reload if needed
|
||||||
# TODO: finish reload feature if needed.
|
# TODO: finish reload feature if needed.
|
||||||
@ -10,8 +10,6 @@ import typing, collections
|
|||||||
if "bpy" in locals():
|
if "bpy" in locals():
|
||||||
import importlib
|
import importlib
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
# we must load icons manager first
|
# we must load icons manager first
|
||||||
# and register it
|
# and register it
|
||||||
from . import UTIL_icons_manager
|
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_UV_flatten_uv, OP_UV_rail_uv
|
||||||
from . import OP_MTL_fix_materials
|
from . import OP_MTL_fix_materials
|
||||||
from . import OP_ADDS_component, OP_ADDS_bme, OP_ADDS_rail
|
from . import OP_ADDS_component, OP_ADDS_bme, OP_ADDS_rail
|
||||||
from . import OP_OBJECT_legacy_align, OP_OBJECT_virtools_group, OP_OBJECT_snoop_group_then_to_mesh, OP_OBJECT_naming_convention
|
from . import OP_OBJECT_legacy_align, OP_OBJECT_virtools_group, OP_OBJECT_snoop_group_then_to_mesh, OP_OBJECT_naming_convention, OP_OBJECT_game_view
|
||||||
|
|
||||||
#region Menu
|
#endregion
|
||||||
|
|
||||||
# ===== Menu Defines =====
|
#region Menu and Sidebar Panel
|
||||||
|
|
||||||
class BBP_MT_View3DMenu(bpy.types.Menu):
|
#region Ballance Adder Menu and Panel
|
||||||
"""Ballance 3D related operators"""
|
|
||||||
bl_idname = "BBP_MT_View3DMenu"
|
|
||||||
bl_label = "Ballance"
|
|
||||||
bl_translation_context = 'BBP_MT_View3DMenu'
|
|
||||||
|
|
||||||
def draw(self, context):
|
class DrawTarget(enum.IntEnum):
|
||||||
layout = self.layout
|
BldMenu = enum.auto()
|
||||||
layout.label(text='UV', icon='UV', text_ctxt='BBP_MT_View3DMenu/draw')
|
BldPanel = enum.auto()
|
||||||
layout.operator(OP_UV_flatten_uv.BBP_OT_flatten_uv.bl_idname)
|
|
||||||
layout.operator(OP_UV_rail_uv.BBP_OT_rail_uv.bl_idname)
|
def reuse_create_layout(layout: bpy.types.UILayout, target: DrawTarget) -> bpy.types.UILayout:
|
||||||
layout.separator()
|
# If we are draw for Panel, we need use Grid to use space enough.
|
||||||
layout.label(text='Align', icon='SNAP_ON', text_ctxt='BBP_MT_View3DMenu/draw')
|
match target:
|
||||||
layout.operator(OP_OBJECT_legacy_align.BBP_OT_legacy_align.bl_idname)
|
case DrawTarget.BldMenu:
|
||||||
layout.separator()
|
return layout
|
||||||
layout.label(text='Select', icon='SELECT_SET', text_ctxt='BBP_MT_View3DMenu/draw')
|
case DrawTarget.BldPanel:
|
||||||
layout.operator(OP_OBJECT_virtools_group.BBP_OT_select_object_by_virtools_group.bl_idname)
|
return layout.grid_flow(even_columns=True, even_rows=True)
|
||||||
layout.separator()
|
|
||||||
layout.label(text='Material', icon='MATERIAL', text_ctxt='BBP_MT_View3DMenu/draw')
|
def reuse_draw_add_bme(layout: bpy.types.UILayout, target: DrawTarget):
|
||||||
layout.operator(OP_MTL_fix_materials.BBP_OT_fix_all_materials.bl_idname)
|
# 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):
|
class BBP_MT_AddBmeMenu(bpy.types.Menu):
|
||||||
"""Add Ballance Floor"""
|
"""Add Ballance Floor"""
|
||||||
@ -59,8 +97,7 @@ class BBP_MT_AddBmeMenu(bpy.types.Menu):
|
|||||||
bl_translation_context = 'BBP_MT_AddBmeMenu'
|
bl_translation_context = 'BBP_MT_AddBmeMenu'
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
reuse_draw_add_bme(self.layout, DrawTarget.BldMenu)
|
||||||
OP_ADDS_bme.BBP_OT_add_bme_struct.draw_blc_menu(layout)
|
|
||||||
|
|
||||||
class BBP_MT_AddRailMenu(bpy.types.Menu):
|
class BBP_MT_AddRailMenu(bpy.types.Menu):
|
||||||
"""Add Ballance Rail"""
|
"""Add Ballance Rail"""
|
||||||
@ -69,54 +106,93 @@ class BBP_MT_AddRailMenu(bpy.types.Menu):
|
|||||||
bl_translation_context = 'BBP_MT_AddRailMenu'
|
bl_translation_context = 'BBP_MT_AddRailMenu'
|
||||||
|
|
||||||
def draw(self, context):
|
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')
|
class BBP_MT_AddComponentMenu(bpy.types.Menu):
|
||||||
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):
|
|
||||||
"""Add Ballance Component"""
|
"""Add Ballance Component"""
|
||||||
bl_idname = "BBP_MT_AddComponentsMenu"
|
bl_idname = "BBP_MT_AddComponentsMenu"
|
||||||
bl_label = "Components"
|
bl_label = "Components"
|
||||||
bl_translation_context = 'BBP_MT_AddComponentsMenu'
|
bl_translation_context = 'BBP_MT_AddComponentsMenu'
|
||||||
|
|
||||||
def draw(self, context):
|
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')
|
class BBP_PT_SidebarAddBmePanel(bpy.types.Panel):
|
||||||
OP_ADDS_component.BBP_OT_add_component.draw_blc_menu(layout)
|
"""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.separator()
|
||||||
layout.label(text="Nong Components", text_ctxt='BBP_MT_AddComponentsMenu/draw')
|
layout.label(text='Align', icon='SNAP_ON', text_ctxt='BBP_MT_View3DMenu/draw')
|
||||||
OP_ADDS_component.BBP_OT_add_nong_extra_point.draw_blc_menu(layout)
|
layout.operator(OP_OBJECT_legacy_align.BBP_OT_legacy_align.bl_idname)
|
||||||
OP_ADDS_component.BBP_OT_add_nong_ventilator.draw_blc_menu(layout)
|
|
||||||
|
|
||||||
layout.separator()
|
layout.separator()
|
||||||
layout.label(text="Series Components", text_ctxt='BBP_MT_AddComponentsMenu/draw')
|
layout.label(text='Camera', icon='CAMERA_DATA', text_ctxt='BBP_MT_View3DMenu/draw')
|
||||||
OP_ADDS_component.BBP_OT_add_tilting_block_series.draw_blc_menu(layout)
|
layout.operator(OP_OBJECT_game_view.BBP_OT_game_resolution.bl_idname)
|
||||||
OP_ADDS_component.BBP_OT_add_swing_series.draw_blc_menu(layout)
|
layout.operator(OP_OBJECT_game_view.BBP_OT_game_camera.bl_idname)
|
||||||
OP_ADDS_component.BBP_OT_add_ventilator_series.draw_blc_menu(layout)
|
|
||||||
|
|
||||||
layout.separator()
|
layout.separator()
|
||||||
layout.label(text="Components Pair", text_ctxt='BBP_MT_AddComponentsMenu/draw')
|
layout.label(text='Select', icon='SELECT_SET', text_ctxt='BBP_MT_View3DMenu/draw')
|
||||||
OP_ADDS_component.BBP_OT_add_sector_component_pair.draw_blc_menu(layout)
|
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:
|
def menu_drawer_import(self, context) -> None:
|
||||||
layout: bpy.types.UILayout = self.layout
|
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.label(text="Ballance", text_ctxt='BBP/__init__.menu_drawer_add()')
|
||||||
layout.menu(BBP_MT_AddBmeMenu.bl_idname, icon='MESH_CUBE')
|
layout.menu(BBP_MT_AddBmeMenu.bl_idname, icon='MESH_CUBE')
|
||||||
layout.menu(BBP_MT_AddRailMenu.bl_idname, icon='MESH_CIRCLE')
|
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:
|
def menu_drawer_grouping(self, context) -> None:
|
||||||
layout: bpy.types.UILayout = self.layout
|
layout: bpy.types.UILayout = self.layout
|
||||||
layout.separator()
|
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.
|
# 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 we create a sub layout and set its operator context as 'INVOKE_DEFAULT',
|
||||||
# thus, all operators can pop up normally.
|
# so that all operators can pop up normally.
|
||||||
col = layout.column()
|
col = layout.column()
|
||||||
col.operator_context = 'INVOKE_DEFAULT'
|
col.operator_context = 'INVOKE_DEFAULT'
|
||||||
|
|
||||||
@ -188,7 +265,8 @@ def menu_drawer_naming_convention(self, context) -> None:
|
|||||||
layout: bpy.types.UILayout = self.layout
|
layout: bpy.types.UILayout = self.layout
|
||||||
layout.separator()
|
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 = layout.column()
|
||||||
col.operator_context = 'INVOKE_DEFAULT'
|
col.operator_context = 'INVOKE_DEFAULT'
|
||||||
|
|
||||||
@ -199,37 +277,43 @@ def menu_drawer_naming_convention(self, context) -> None:
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region Register and Unregister.
|
#region Register and Unregister.
|
||||||
|
|
||||||
g_BldClasses: tuple[typing.Any, ...] = (
|
g_BldClasses: tuple[typing.Any, ...] = (
|
||||||
BBP_MT_View3DMenu,
|
BBP_MT_View3DMenu,
|
||||||
BBP_MT_AddBmeMenu,
|
BBP_MT_AddBmeMenu,
|
||||||
BBP_MT_AddRailMenu,
|
BBP_MT_AddRailMenu,
|
||||||
BBP_MT_AddComponentsMenu
|
BBP_MT_AddComponentMenu,
|
||||||
|
|
||||||
|
BBP_PT_SidebarAddBmePanel,
|
||||||
|
BBP_PT_SidebarAddRailPanel,
|
||||||
|
BBP_PT_SidebarAddComponentPanel,
|
||||||
)
|
)
|
||||||
|
|
||||||
class MenuEntry():
|
class MenuEntry():
|
||||||
mContainerMenu: bpy.types.Menu
|
mContainerMenu: bpy.types.Menu
|
||||||
mIsAppend: bool
|
mIsAppend: bool
|
||||||
mMenuDrawer: MenuDrawer_t
|
mMenuDrawer: TFctMenuDrawer
|
||||||
def __init__(self, cont: bpy.types.Menu, is_append: bool, menu_func: MenuDrawer_t):
|
def __init__(self, cont: bpy.types.Menu, is_append: bool, menu_func: TFctMenuDrawer):
|
||||||
self.mContainerMenu = cont
|
self.mContainerMenu = cont
|
||||||
self.mIsAppend = is_append
|
self.mIsAppend = is_append
|
||||||
self.mMenuDrawer = menu_func
|
self.mMenuDrawer = menu_func
|
||||||
|
|
||||||
g_BldMenus: tuple[MenuEntry, ...] = (
|
g_BldMenus: tuple[MenuEntry, ...] = (
|
||||||
MenuEntry(bpy.types.VIEW3D_MT_editor_menus, False, menu_drawer_view3d),
|
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_import, True, menu_drawer_import),
|
||||||
MenuEntry(bpy.types.TOPBAR_MT_file_export, True, menu_drawer_export),
|
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_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)
|
# Register this twice (for 2 menus respectively)
|
||||||
MenuEntry(bpy.types.VIEW3D_MT_object_context_menu, True, menu_drawer_grouping),
|
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_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:
|
def register() -> None:
|
||||||
@ -266,6 +350,7 @@ def register() -> None:
|
|||||||
OP_OBJECT_virtools_group.register()
|
OP_OBJECT_virtools_group.register()
|
||||||
OP_OBJECT_snoop_group_then_to_mesh.register()
|
OP_OBJECT_snoop_group_then_to_mesh.register()
|
||||||
OP_OBJECT_naming_convention.register()
|
OP_OBJECT_naming_convention.register()
|
||||||
|
OP_OBJECT_game_view.register()
|
||||||
|
|
||||||
# register other classes
|
# register other classes
|
||||||
for cls in g_BldClasses:
|
for cls in g_BldClasses:
|
||||||
@ -288,6 +373,7 @@ def unregister() -> None:
|
|||||||
bpy.utils.unregister_class(cls)
|
bpy.utils.unregister_class(cls)
|
||||||
|
|
||||||
# unregister modules
|
# unregister modules
|
||||||
|
OP_OBJECT_game_view.unregister()
|
||||||
OP_OBJECT_naming_convention.unregister()
|
OP_OBJECT_naming_convention.unregister()
|
||||||
OP_OBJECT_snoop_group_then_to_mesh.unregister()
|
OP_OBJECT_snoop_group_then_to_mesh.unregister()
|
||||||
OP_OBJECT_virtools_group.unregister()
|
OP_OBJECT_virtools_group.unregister()
|
||||||
|
@ -37,7 +37,7 @@ license = [
|
|||||||
# ]
|
# ]
|
||||||
|
|
||||||
# Optional list of supported platforms. If omitted, the extension will be available in all operating systems.
|
# Optional list of supported platforms. If omitted, the extension will be available in all operating systems.
|
||||||
platforms = ["windows-x64", "linux-x64"]
|
platforms = ["windows-x64", "linux-x64", "macos-arm64"]
|
||||||
# Supported platforms: "windows-x64", "macos-arm64", "linux-x64", "windows-arm64", "macos-x64"
|
# Supported platforms: "windows-x64", "macos-arm64", "linux-x64", "windows-arm64", "macos-x64"
|
||||||
|
|
||||||
# Optional: bundle 3rd party Python modules.
|
# Optional: bundle 3rd party Python modules.
|
||||||
|
442
i18n/blender.pot
442
i18n/blender.pot
File diff suppressed because it is too large
Load Diff
442
i18n/zh_HANS.po
442
i18n/zh_HANS.po
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,4 @@
|
|||||||
# Tools
|
# Scripts
|
||||||
|
|
||||||
These tool scripts is served for BBP_NG specifically. We use Astral UV with a single Python project file to manage these tools. You can browse their usage in this file.
|
These tool scripts is served for BBP_NG specifically. We use Astral UV with a single Python project file to manage these tools. You can browse their usage in this file.
|
||||||
|
|
||||||
@ -14,10 +14,18 @@ Compress BME prototype JSON files into smaller size.
|
|||||||
|
|
||||||
Execute `uv run build_json.py`
|
Execute `uv run build_json.py`
|
||||||
|
|
||||||
|
## Build Meshes
|
||||||
|
|
||||||
|
Copy Ballance element placeholder into Blender plugin.
|
||||||
|
|
||||||
|
Execute `uv run build_meshes.py`
|
||||||
|
|
||||||
## Validate BME Prototype
|
## Validate BME Prototype
|
||||||
|
|
||||||
Validate the correction of BME prorotype JSON files.
|
Validate the correction of BME prorotype JSON files.
|
||||||
|
|
||||||
|
Validation is VERY crucial. Before running anything involving BME prototype JSONs, please validate them first.
|
||||||
|
|
||||||
Execute `uv run validate_json.py`
|
Execute `uv run validate_json.py`
|
||||||
|
|
||||||
## Extract BME Translation
|
## Extract BME Translation
|
||||||
|
@ -1,3 +1,78 @@
|
|||||||
import enum
|
import enum
|
||||||
from typing import Optional, Self
|
from typing import Optional
|
||||||
from pydantic import BaseModel, RootModel, Field, model_validator, ValidationError
|
from pydantic import BaseModel, RootModel, Field, model_validator, ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class ShowcaseType(enum.StrEnum):
|
||||||
|
Nothing = 'none'
|
||||||
|
Floor = 'floor'
|
||||||
|
Rail = 'Rail'
|
||||||
|
Wood = 'wood'
|
||||||
|
|
||||||
|
|
||||||
|
class ShowcaseCfgType(enum.StrEnum):
|
||||||
|
Float = 'float'
|
||||||
|
Int = 'int'
|
||||||
|
Bool = 'bool'
|
||||||
|
Face = 'face'
|
||||||
|
|
||||||
|
|
||||||
|
class ShowcaseCfg(BaseModel):
|
||||||
|
field: str = Field(frozen=True, strict=True)
|
||||||
|
type: ShowcaseCfgType = Field(frozen=True)
|
||||||
|
title: str = Field(frozen=True, strict=True)
|
||||||
|
desc: str = Field(frozen=True, strict=True)
|
||||||
|
default: str = Field(frozen=True, strict=True)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
class Param(BaseModel):
|
||||||
|
field: str = Field(frozen=True, strict=True)
|
||||||
|
data: str = Field(frozen=True, strict=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Var(BaseModel):
|
||||||
|
field: str = Field(frozen=True, strict=True)
|
||||||
|
data: str = Field(frozen=True, strict=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Vertex(BaseModel):
|
||||||
|
skip: str = Field(frozen=True, strict=True)
|
||||||
|
data: str = Field(frozen=True, strict=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Face(BaseModel):
|
||||||
|
skip: str = Field(frozen=True, strict=True)
|
||||||
|
texture: str = Field(frozen=True, strict=True)
|
||||||
|
indices: list[int] = Field(frozen=True, strict=True)
|
||||||
|
uvs: list[str] = Field(frozen=True, strict=True)
|
||||||
|
normals: Optional[list[str]] = Field(frozen=True, strict=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Instance(BaseModel):
|
||||||
|
identifier: str = Field(frozen=True, strict=True)
|
||||||
|
skip: str = Field(frozen=True, strict=True)
|
||||||
|
params: dict[str, str] = Field(frozen=True, strict=True)
|
||||||
|
transform: str = Field(frozen=True, strict=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Prototype(BaseModel):
|
||||||
|
identifier: str = Field(frozen=True, strict=True)
|
||||||
|
showcase: Optional[Showcase] = Field(frozen=True, strict=True)
|
||||||
|
params: list[Param] = Field(frozen=True, strict=True)
|
||||||
|
skip: str = Field(frozen=True, strict=True)
|
||||||
|
vars: list[Var] = Field(frozen=True, strict=True)
|
||||||
|
vertices: list[Vertex] = Field(frozen=True, strict=True)
|
||||||
|
faces: list[Face] = Field(frozen=True, strict=True)
|
||||||
|
instances: list[Instance] = Field(frozen=True, strict=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Prototypes(RootModel):
|
||||||
|
root: list[Prototype] = Field(frozen=True, strict=True)
|
||||||
|
@ -1,433 +0,0 @@
|
|||||||
import typing
|
|
||||||
import simple_po, bme_utils
|
|
||||||
|
|
||||||
#region Translation Constant
|
|
||||||
|
|
||||||
## TODO:
|
|
||||||
# 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'
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region BME Tokens
|
|
||||||
|
|
||||||
## TODO:
|
|
||||||
# These token are copied from UTIL_bme.py.
|
|
||||||
# If anything changed, such as BME standard, these tokens should be synchronized between these 2 modules.
|
|
||||||
|
|
||||||
TOKEN_IDENTIFIER: str = 'identifier'
|
|
||||||
|
|
||||||
TOKEN_SHOWCASE: str = 'showcase'
|
|
||||||
TOKEN_SHOWCASE_TITLE: str = 'title'
|
|
||||||
TOKEN_SHOWCASE_ICON: str = 'icon'
|
|
||||||
TOKEN_SHOWCASE_TYPE: str = 'type'
|
|
||||||
TOKEN_SHOWCASE_CFGS: str = 'cfgs'
|
|
||||||
TOKEN_SHOWCASE_CFGS_FIELD: str = 'field'
|
|
||||||
TOKEN_SHOWCASE_CFGS_TYPE: str = 'type'
|
|
||||||
TOKEN_SHOWCASE_CFGS_TITLE: str = 'title'
|
|
||||||
TOKEN_SHOWCASE_CFGS_DESC: str = 'desc'
|
|
||||||
TOKEN_SHOWCASE_CFGS_DEFAULT: str = 'default'
|
|
||||||
|
|
||||||
TOKEN_SKIP: str = 'skip'
|
|
||||||
|
|
||||||
TOKEN_PARAMS: str = 'params'
|
|
||||||
TOKEN_PARAMS_FIELD: str = 'field'
|
|
||||||
TOKEN_PARAMS_DATA: str = 'data'
|
|
||||||
|
|
||||||
TOKEN_VARS: str = 'vars'
|
|
||||||
TOKEN_VARS_FIELD: str = 'field'
|
|
||||||
TOKEN_VARS_DATA: str = 'data'
|
|
||||||
|
|
||||||
TOKEN_VERTICES: str = 'vertices'
|
|
||||||
TOKEN_VERTICES_SKIP: str = 'skip'
|
|
||||||
TOKEN_VERTICES_DATA: str = 'data'
|
|
||||||
|
|
||||||
TOKEN_FACES: str = 'faces'
|
|
||||||
TOKEN_FACES_SKIP: str = 'skip'
|
|
||||||
TOKEN_FACES_TEXTURE: str = 'texture'
|
|
||||||
TOKEN_FACES_INDICES: str = 'indices'
|
|
||||||
TOKEN_FACES_UVS: str = 'uvs'
|
|
||||||
TOKEN_FACES_NORMALS: str = 'normals'
|
|
||||||
|
|
||||||
TOKEN_INSTANCES: str = 'instances'
|
|
||||||
TOKEN_INSTANCES_IDENTIFIER: str = 'identifier'
|
|
||||||
TOKEN_INSTANCES_SKIP: str = 'skip'
|
|
||||||
TOKEN_INSTANCES_PARAMS: str = 'params'
|
|
||||||
TOKEN_INSTANCES_TRANSFORM: str = 'transform'
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
# TODO: finish BME validator
|
|
||||||
|
|
||||||
# class ReporterWithHierarchy():
|
|
||||||
# """
|
|
||||||
# BME validator and extractor specifically used reporter
|
|
||||||
# which auotmatically use hierarchy as its context when outputing.
|
|
||||||
# """
|
|
||||||
|
|
||||||
# __mReporter: bme_utils.Reporter
|
|
||||||
# __mHierarchy: bme_utils.Hierarchy
|
|
||||||
|
|
||||||
# def __init__(self, reporter: bme_utils.Reporter, hierarchy: bme_utils.Hierarchy):
|
|
||||||
# self.__mReporter = reporter
|
|
||||||
# self.__mHierarchy = hierarchy
|
|
||||||
|
|
||||||
# def error(self, msg: str) -> None:
|
|
||||||
# self.__mReporter.error(msg, self.__mHierarchy.build_hierarchy_string())
|
|
||||||
# def warning(self, msg: str) -> None:
|
|
||||||
# self.__mReporter.warning(msg, self.__mHierarchy.build_hierarchy_string())
|
|
||||||
# def info(self, msg: str) -> None:
|
|
||||||
# self.__mReporter.info(msg, self.__mHierarchy.build_hierarchy_string())
|
|
||||||
|
|
||||||
# class UniqueField():
|
|
||||||
# """
|
|
||||||
# Some BME prototype fields should be unique in globl scope.
|
|
||||||
# So BME validator should check this. That's the feature this class provided.
|
|
||||||
|
|
||||||
# This class is an abstract class and should not be used directly.
|
|
||||||
# Use child class please.
|
|
||||||
# """
|
|
||||||
|
|
||||||
# __mUniques: set[str]
|
|
||||||
# __mReporter: ReporterWithHierarchy
|
|
||||||
|
|
||||||
# def __init__(self, reporter: ReporterWithHierarchy):
|
|
||||||
# self.__mUniques = set()
|
|
||||||
# self.__mReporter = reporter
|
|
||||||
|
|
||||||
# def register(self, entry: str) -> bool:
|
|
||||||
# """
|
|
||||||
# @brief Try to register given entry in unique.
|
|
||||||
# @details
|
|
||||||
# If given entry is not presented in unique set, given entry will be inserted and return True.
|
|
||||||
# If given entry is already available in unique set, this function will use reporter to output an error message and return False.
|
|
||||||
# @param[in] entry The entry to be checked and inserted.
|
|
||||||
# @return True if entry is unique, otherwise false.
|
|
||||||
# """
|
|
||||||
# if entry in self.__mUniques:
|
|
||||||
# self.__mReporter.error(self._get_error_msg(entry))
|
|
||||||
# return False
|
|
||||||
# else:
|
|
||||||
# self.__mUniques.add(entry)
|
|
||||||
# return True
|
|
||||||
|
|
||||||
# def clear(self) -> None:
|
|
||||||
# """
|
|
||||||
# @brief Clear this unique set for further using.
|
|
||||||
# """
|
|
||||||
# self.__mUniques.clear()
|
|
||||||
|
|
||||||
# def _get_error_msg(self, err_entry: str) -> str:
|
|
||||||
# """
|
|
||||||
# @brief Get the error message when error occurs.
|
|
||||||
# @details
|
|
||||||
# This is internal used function to get the error message which will be passed to reporter.
|
|
||||||
# This message is generated by given entry which cause the non-unique issue.
|
|
||||||
# Outer caller should not call this function and every child class should override this function.
|
|
||||||
# @param[in] err_entry The entry cause the error.
|
|
||||||
# @return The error message generated from given error entry.
|
|
||||||
# """
|
|
||||||
# raise NotImplementedError()
|
|
||||||
|
|
||||||
# class UniqueIdentifier(UniqueField):
|
|
||||||
# """Specific UniqueField for unique prototype identifier."""
|
|
||||||
# def _get_error_msg(self, err_entry: str) -> str:
|
|
||||||
# return f'Trying to register multiple prototype with same name: "{err_entry}".'
|
|
||||||
# class UniqueVariable(UniqueField):
|
|
||||||
# """Specific UniqueField for unique variable names within prototype."""
|
|
||||||
# def _get_error_msg(self, err_entry: str) -> str:
|
|
||||||
# return f'Trying to define multiple variable with same name: "{err_entry}" in the same prototype.'
|
|
||||||
|
|
||||||
# class BMEValidator():
|
|
||||||
# """
|
|
||||||
# The validator for BME prototype declarartions.
|
|
||||||
# This validator will validate given prototype declaration JSON structure,
|
|
||||||
# to check then whether have all essential fields BME standard required and whether have any unknown fields.
|
|
||||||
# """
|
|
||||||
|
|
||||||
# __mHierarchy: bme_utils.Hierarchy
|
|
||||||
# __mReporter: ReporterWithHierarchy
|
|
||||||
|
|
||||||
# __mUniqueIdentifier: UniqueIdentifier
|
|
||||||
# __mUniqueVariable: UniqueVariable
|
|
||||||
|
|
||||||
# def __init__(self, reporter: bme_utils.Reporter):
|
|
||||||
# self.__mHierarchy = bme_utils.Hierarchy()
|
|
||||||
# self.__mReporter = ReporterWithHierarchy(reporter, self.__mHierarchy)
|
|
||||||
|
|
||||||
# self.__mUniqueIdentifier = UniqueIdentifier(self.__mReporter)
|
|
||||||
# self.__mUniqueVariable = UniqueVariable(self.__mReporter)
|
|
||||||
|
|
||||||
# _TCheckKey = typing.TypeVar('_TCheckKey')
|
|
||||||
# def __check_key(self, data: dict[str, typing.Any], key: str, expected_type: type[_TCheckKey]) -> _TCheckKey | None:
|
|
||||||
# """
|
|
||||||
# @brief Check the existance and tyoe of value stored in given dict and key.
|
|
||||||
# @param[in] data The dict need to be checked
|
|
||||||
# @param[in] key The key for fetching value.
|
|
||||||
# @param[in] expected_type The expected type of fetched value.
|
|
||||||
# @return None if error occurs, otherwise the value stored in given dict and key.
|
|
||||||
# """
|
|
||||||
# gotten_value = data[key]
|
|
||||||
# if gotten_value is None:
|
|
||||||
# # report no key error
|
|
||||||
# self.__mReporter.error(f'Can not find key "{key}". Did you forget it?')
|
|
||||||
# elif not isinstance(gotten_value, expected_type):
|
|
||||||
# # get the type of value
|
|
||||||
# value_type = type(gotten_value)
|
|
||||||
# # format normal error message
|
|
||||||
# err_msg: str = f'The type of value stored inside key "{key}" is incorrect. '
|
|
||||||
# err_msg += f'Expect "{expected_type.__name__}" got "{value_type.__name__}". '
|
|
||||||
# # add special note for easily confusing types
|
|
||||||
# # e.g. forget quote number (number literal are recognise as number accidently)
|
|
||||||
# if issubclass(expected_type, str) and issubclass(type(data), (int, float)):
|
|
||||||
# err_msg += 'Did you forgot quote the number?'
|
|
||||||
# # report type error
|
|
||||||
# self.__mReporter.error(err_msg)
|
|
||||||
# else:
|
|
||||||
# # no error, return value
|
|
||||||
# return gotten_value
|
|
||||||
# # error occurs, return null
|
|
||||||
# return None
|
|
||||||
|
|
||||||
# def __check_self(self, data: typing.Any, expected_type: type) -> bool:
|
|
||||||
# """
|
|
||||||
# @brief Check the type of given data.
|
|
||||||
# @return True if type matched, otherwise false.
|
|
||||||
# """
|
|
||||||
# if data is None:
|
|
||||||
# self.__mReporter.error('Data is unexpected null.')
|
|
||||||
# elif not isinstance(data, expected_type):
|
|
||||||
# # usually this function is checking list or dict, so no scenario that user forget quote literal number.
|
|
||||||
# self.__mReporter.error(f'The type of given data is not expected. Expect "{expected_type.__name__}" got "{type(data).__name__}".')
|
|
||||||
# else:
|
|
||||||
# # no error, return okey
|
|
||||||
# return True
|
|
||||||
# # error occurs, return failed
|
|
||||||
# return False
|
|
||||||
|
|
||||||
# # 按层次递归调用检查。
|
|
||||||
# # 每个层次只负责当前层次的检查。
|
|
||||||
# # 如果值为列表,字典,则在当前层次检查完其类型(容器本身,对每一项不检查),然后对每一项调用对应层次检查。
|
|
||||||
# # 如果值不是上述类型(例如整数,浮点数,字符串等),在当前层次检查。
|
|
||||||
|
|
||||||
# def validate(self, assoc_file: str, prototypes: typing.Any) -> None:
|
|
||||||
# # reset hierarchy
|
|
||||||
# self.__mHierarchy.clear()
|
|
||||||
# # start to validate
|
|
||||||
# with self.__mHierarchy.safe_push(assoc_file):
|
|
||||||
# self.__validate_prototypes(prototypes)
|
|
||||||
|
|
||||||
# def __validate_prototypes(self, prototypes: typing.Any) -> None:
|
|
||||||
# # the most outer structure must be a list
|
|
||||||
# if not self.__check_self(prototypes, list): return
|
|
||||||
# cast_prototypes = typing.cast(list[typing.Any], prototypes)
|
|
||||||
# # iterate prototype
|
|
||||||
# for prototype_index, prototype in enumerate(cast_prototypes):
|
|
||||||
# with self.__mHierarchy.safe_push(prototype_index) as layer:
|
|
||||||
# self.__validate_prototype(layer, prototype)
|
|
||||||
|
|
||||||
# def __validate_prototype(self, layer: bme_utils.HierarchyLayer, prototype: typing.Any) -> None:
|
|
||||||
# # check whether self is a dict
|
|
||||||
# if not self.__check_self(prototype, dict): return
|
|
||||||
# cast_prototype = typing.cast(dict[str, typing.Any], prototype)
|
|
||||||
|
|
||||||
# # clear unique field for each prototype
|
|
||||||
# self.__mUniqueVariable.clear()
|
|
||||||
|
|
||||||
# # check identifier
|
|
||||||
# identifier = self.__check_key(cast_prototype, TOKEN_IDENTIFIER, str)
|
|
||||||
# if identifier is not None:
|
|
||||||
# # replace hierarchy
|
|
||||||
# layer.emplace(identifier)
|
|
||||||
# # check unique
|
|
||||||
# self.__mUniqueIdentifier.register(identifier)
|
|
||||||
|
|
||||||
# # check showcase but don't use check function
|
|
||||||
# # because it is optional.
|
|
||||||
# showcase = cast_prototype[TOKEN_SHOWCASE]
|
|
||||||
# if showcase is not None:
|
|
||||||
# # we only check non-template prototype
|
|
||||||
# with self.__mHierarchy.safe_push(TOKEN_SHOWCASE):
|
|
||||||
# self.__validate_showcase(typing.cast(dict[str, typing.Any], showcase))
|
|
||||||
|
|
||||||
# # check params, vars, vertices, faces, instances
|
|
||||||
# # they are all list
|
|
||||||
# params = self.__check_key(cast_prototype, TOKEN_PARAMS, list)
|
|
||||||
# if params is not None:
|
|
||||||
# cast_params = typing.cast(list[typing.Any], params)
|
|
||||||
# with self.__mHierarchy.safe_push(TOKEN_PARAMS):
|
|
||||||
# for param_index, param in enumerate(cast_params):
|
|
||||||
# with self.__mHierarchy.safe_push(param_index):
|
|
||||||
# self.__validate_param(param)
|
|
||||||
|
|
||||||
# vars = self.__check_key(cast_prototype, TOKEN_VARS, list)
|
|
||||||
# if vars is not None:
|
|
||||||
# cast_vars = typing.cast(list[typing.Any], vars)
|
|
||||||
# with self.__mHierarchy.safe_push(TOKEN_VARS):
|
|
||||||
# for var_index, var in enumerate(cast_vars):
|
|
||||||
# with self.__mHierarchy.safe_push(var_index):
|
|
||||||
# self.__validate_var(var)
|
|
||||||
|
|
||||||
# vertices = self.__check_key(cast_prototype, TOKEN_VERTICES, list)
|
|
||||||
# if vertices is not None:
|
|
||||||
# cast_vertices = typing.cast(list[typing.Any], vertices)
|
|
||||||
# with self.__mHierarchy.safe_push(TOKEN_VERTICES):
|
|
||||||
# for vertex_index, vertex in enumerate(cast_vertices):
|
|
||||||
# with self.__mHierarchy.safe_push(vertex_index):
|
|
||||||
# self.__validate_vertex(vertex)
|
|
||||||
|
|
||||||
# faces = self.__check_key(cast_prototype, TOKEN_FACES, list)
|
|
||||||
# if faces is not None:
|
|
||||||
# cast_faces = typing.cast(list[typing.Any], faces)
|
|
||||||
# with self.__mHierarchy.safe_push(TOKEN_FACES):
|
|
||||||
# for face_index, face in enumerate(cast_faces):
|
|
||||||
# with self.__mHierarchy.safe_push(face_index):
|
|
||||||
# self.__validate_face(face)
|
|
||||||
|
|
||||||
# instances = self.__check_key(cast_prototype, TOKEN_INSTANCES, list)
|
|
||||||
# if instances is not None:
|
|
||||||
# cast_instances = typing.cast(list[typing.Any], instances)
|
|
||||||
# with self.__mHierarchy.safe_push(TOKEN_INSTANCES):
|
|
||||||
# for instance_index, instance in enumerate(cast_instances):
|
|
||||||
# with self.__mHierarchy.safe_push(instance_index):
|
|
||||||
# self.__validate_instance(instance)
|
|
||||||
|
|
||||||
# def __validate_showcase(self, showcase: dict[str, typing.Any]) -> None:
|
|
||||||
# pass
|
|
||||||
|
|
||||||
# def __validate_param(self, param: typing.Any) -> None:
|
|
||||||
# # check whether self is a dict
|
|
||||||
# if not self.__check_self(param, dict): return
|
|
||||||
# cast_param = typing.cast(dict[str, typing.Any], param)
|
|
||||||
|
|
||||||
# # check field
|
|
||||||
# field = self.__check_key(cast_param, TOKEN_PARAMS_FIELD, str)
|
|
||||||
# if field is not None:
|
|
||||||
# self.__mUniqueVariable.register(field)
|
|
||||||
|
|
||||||
# # check data
|
|
||||||
# self.__check_key(cast_param, TOKEN_PARAMS_DATA, str)
|
|
||||||
|
|
||||||
# def __validate_var(self, var: typing.Any) -> None:
|
|
||||||
# # check whether self is a dict
|
|
||||||
# if not self.__check_self(var, dict): return
|
|
||||||
# cast_var = typing.cast(dict[str, typing.Any], var)
|
|
||||||
|
|
||||||
# # check field
|
|
||||||
# field = self.__check_key(cast_var, TOKEN_VARS_FIELD, str)
|
|
||||||
# if field is not None:
|
|
||||||
# self.__mUniqueVariable.register(field)
|
|
||||||
|
|
||||||
# # check data
|
|
||||||
# self.__check_key(cast_var, TOKEN_VARS_DATA, str)
|
|
||||||
|
|
||||||
# def __validate_vertex(self, vertex: typing.Any) -> None:
|
|
||||||
# # check whether self is a dict
|
|
||||||
# if not self.__check_self(vertex, dict): return
|
|
||||||
# cast_vertex = typing.cast(dict[str, typing.Any], vertex)
|
|
||||||
|
|
||||||
# # check fields
|
|
||||||
# self.__check_key(cast_vertex, TOKEN_VERTICES_SKIP, str)
|
|
||||||
# self.__check_key(cast_vertex, TOKEN_VERTICES_DATA, str)
|
|
||||||
|
|
||||||
# def __validate_face(self, face: typing.Any) -> None:
|
|
||||||
# pass
|
|
||||||
|
|
||||||
# def __validate_instance(self, instance: typing.Any) -> None:
|
|
||||||
# pass
|
|
||||||
|
|
||||||
|
|
||||||
class BMEExtractor():
|
|
||||||
"""
|
|
||||||
A GetText extractor for BME prototype declarations.
|
|
||||||
This extractor can extract all UI infomations which will be shown on Blender first.
|
|
||||||
Then write them into caller given PO file. So that translator can translate them.
|
|
||||||
|
|
||||||
Blender default I18N plugin can not recognise these dynamic loaded content,
|
|
||||||
so that's the reason why this class invented.
|
|
||||||
|
|
||||||
Please note all data should be validate first, then pass to this class.
|
|
||||||
Otherwise it is undefined behavior.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__mAssocFile: str
|
|
||||||
__mHierarchy: bme_utils.Hierarchy
|
|
||||||
__mReporter: bme_utils.Reporter
|
|
||||||
__mPoWriter: simple_po.PoWriter
|
|
||||||
|
|
||||||
def __init__(self, reporter: bme_utils.Reporter, po_writer: simple_po.PoWriter):
|
|
||||||
self.__mAssocFile = ''
|
|
||||||
self.__mHierarchy = bme_utils.Hierarchy()
|
|
||||||
self.__mReporter = reporter
|
|
||||||
self.__mPoWriter = po_writer
|
|
||||||
|
|
||||||
def __add_translation(self, msg: str) -> None:
|
|
||||||
"""
|
|
||||||
@brief Convenient internal translation adder.
|
|
||||||
@details Add given message into PO file with auto generated hierarchy for translation context.
|
|
||||||
@param[in] msg The message for translating.
|
|
||||||
"""
|
|
||||||
self.__mPoWriter.add_entry(
|
|
||||||
msg,
|
|
||||||
CTX_TRANSLATION + '/' + self.__mHierarchy.build_hierarchy_string(),
|
|
||||||
# use associated file as extracted message to tell user where we extract it.
|
|
||||||
# put file name in hierarchy is not proper (file path may be changed when moving prototype between them).
|
|
||||||
self.__mAssocFile
|
|
||||||
)
|
|
||||||
|
|
||||||
def __report_duplication_error(self) -> None:
|
|
||||||
"""
|
|
||||||
@brief Convenient internal function to report duplicated translation message issue.
|
|
||||||
@details
|
|
||||||
A convenient internal used function to report issue that
|
|
||||||
the "title" field and "desc" field of the same showcase configuration entry have same content
|
|
||||||
which may cause that generated PO file is illegal.
|
|
||||||
"""
|
|
||||||
self.__mReporter.error(
|
|
||||||
'The content of "title" and "desc" can not be the same in one entry. Please modify one of them.',
|
|
||||||
self.__mAssocFile + '/' + self.__mHierarchy.build_hierarchy_string()
|
|
||||||
)
|
|
||||||
|
|
||||||
def extract(self, assoc_file: str, prototypes: list[dict[str, typing.Any]]) -> None:
|
|
||||||
self.__mAssocFile = assoc_file
|
|
||||||
for prototype in prototypes:
|
|
||||||
self.__extract_prototype(prototype)
|
|
||||||
|
|
||||||
def __extract_prototype(self, prototype: dict[str, typing.Any]) -> None:
|
|
||||||
# get identifier first
|
|
||||||
identifier: str = prototype[TOKEN_IDENTIFIER]
|
|
||||||
with self.__mHierarchy.safe_push(identifier):
|
|
||||||
# get showcase node and only write PO file if it is not template prototype
|
|
||||||
showcase: dict[str, typing.Any] | None = prototype[TOKEN_SHOWCASE]
|
|
||||||
if showcase is not None:
|
|
||||||
self.__extract_showcase(showcase)
|
|
||||||
|
|
||||||
def __extract_showcase(self, showcase: dict[str, typing.Any]) -> None:
|
|
||||||
# export self name first
|
|
||||||
self.__add_translation(showcase[TOKEN_SHOWCASE_TITLE])
|
|
||||||
|
|
||||||
# iterate cfgs
|
|
||||||
cfgs: list[dict[str, typing.Any]] = showcase[TOKEN_SHOWCASE_CFGS]
|
|
||||||
for cfg_index, cfg in enumerate(cfgs):
|
|
||||||
self.__extract_showcase_cfg(cfg_index, cfg)
|
|
||||||
|
|
||||||
def __extract_showcase_cfg(self, index: int, cfg: dict[str, typing.Any]) -> None:
|
|
||||||
# push cfg index
|
|
||||||
with self.__mHierarchy.safe_push(index):
|
|
||||||
# extract field title and description
|
|
||||||
title: str = cfg[TOKEN_SHOWCASE_CFGS_TITLE]
|
|
||||||
desc: str = cfg[TOKEN_SHOWCASE_CFGS_DESC]
|
|
||||||
|
|
||||||
# check duplication error
|
|
||||||
# if "title" is equal to "desc" and they are not blank
|
|
||||||
if title == desc and title != "":
|
|
||||||
self.__report_duplication_error()
|
|
||||||
|
|
||||||
# export them respectively if they are not blank
|
|
||||||
if title != "":
|
|
||||||
self.__add_translation(title)
|
|
||||||
if desc!= "":
|
|
||||||
self.__add_translation(desc)
|
|
||||||
|
|
@ -1,144 +0,0 @@
|
|||||||
import typing
|
|
||||||
import collections
|
|
||||||
import termcolor
|
|
||||||
|
|
||||||
class Reporter():
|
|
||||||
"""
|
|
||||||
General reporter with context support for convenient logging.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __report(self, type: str, msg: str, context: str | None, color: str) -> None:
|
|
||||||
# build message
|
|
||||||
strl: str = f'[{type}]'
|
|
||||||
if context is not None:
|
|
||||||
strl += f'[{context}]'
|
|
||||||
strl += ' ' + msg
|
|
||||||
# output with color
|
|
||||||
termcolor.cprint(strl, color)
|
|
||||||
|
|
||||||
def error(self, msg: str, context: str | None = None) -> None:
|
|
||||||
"""
|
|
||||||
@brief Report an error.
|
|
||||||
@param[in] msg The message to show.
|
|
||||||
@param[in] context The context of this message, e.g. the file path. None if no context.
|
|
||||||
"""
|
|
||||||
self.__report('Error', msg, context, 'red')
|
|
||||||
|
|
||||||
def warning(self, msg: str, context: str | None = None) -> None:
|
|
||||||
"""
|
|
||||||
@brief Report a warning.
|
|
||||||
@param[in] msg The message to show.
|
|
||||||
@param[in] context The context of this message, e.g. the file path. None if no context.
|
|
||||||
"""
|
|
||||||
self.__report('Warning', msg, context, 'yellow')
|
|
||||||
|
|
||||||
def info(self, msg: str, context: str | None = None) -> None:
|
|
||||||
"""
|
|
||||||
@brief Report a info.
|
|
||||||
@param[in] msg The message to show.
|
|
||||||
@param[in] context The context of this message, e.g. the file path. None if no context.
|
|
||||||
"""
|
|
||||||
self.__report('Info', msg, context, 'white')
|
|
||||||
|
|
||||||
class Hierarchy():
|
|
||||||
"""
|
|
||||||
The hierarchy for BME validator and BME extractor.
|
|
||||||
In BME validator, it build human-readable string representing the location where error happen.
|
|
||||||
In BME extractor, it build the string used as the context of translation.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__mStack: collections.deque[str]
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.__mStack = collections.deque()
|
|
||||||
|
|
||||||
def push(self, item: str | int) -> None:
|
|
||||||
"""
|
|
||||||
@brief Add an item into the top of this hierarchy.
|
|
||||||
@details
|
|
||||||
If given item is string, it will be push into hierarchy directly.
|
|
||||||
If given item is integer, this function will treat it as a special case, the index.
|
|
||||||
Function will push it into hierarchy after formatting it (add a pair of bracket around it).
|
|
||||||
@param[in] item New added item.
|
|
||||||
"""
|
|
||||||
if isinstance(item, str):
|
|
||||||
self.__mStack.append(item)
|
|
||||||
elif isinstance(item, int):
|
|
||||||
self.__mStack.append(f'[{item}]')
|
|
||||||
else:
|
|
||||||
raise Exception('Unexpected type of item when pushing into hierarchy.')
|
|
||||||
|
|
||||||
def pop(self) -> None:
|
|
||||||
"""
|
|
||||||
@brief Remove the top item from hierarchy
|
|
||||||
"""
|
|
||||||
self.__mStack.pop()
|
|
||||||
|
|
||||||
def safe_push(self, item: str | int) -> 'HierarchyLayer':
|
|
||||||
"""
|
|
||||||
@brief The safe version of push function.
|
|
||||||
@return A with-context-supported instance which can make sure pushed item popped when leaving scope.
|
|
||||||
"""
|
|
||||||
return HierarchyLayer(self, item)
|
|
||||||
|
|
||||||
def clear(self) -> None:
|
|
||||||
"""
|
|
||||||
@brief Clear this hierarchy.
|
|
||||||
"""
|
|
||||||
self.__mStack.clear()
|
|
||||||
|
|
||||||
def depth(self) -> int:
|
|
||||||
"""
|
|
||||||
@brief Return the depth of this hierarchy.
|
|
||||||
@return The depth of this hierarchy.
|
|
||||||
"""
|
|
||||||
return len(self.__mStack)
|
|
||||||
|
|
||||||
def build_hierarchy_string(self) -> str:
|
|
||||||
"""
|
|
||||||
@brief Build the string which can represent this hierarchy.
|
|
||||||
@details It just join every items with `/` as separator.
|
|
||||||
@return The built string representing this hierarchy.
|
|
||||||
"""
|
|
||||||
return '/'.join(self.__mStack)
|
|
||||||
|
|
||||||
class HierarchyLayer():
|
|
||||||
"""
|
|
||||||
An with-context-supported class for Hierarchy which can automatically pop item when leaving scope.
|
|
||||||
This is convenient for keeping the balance of Hierarchy (avoid programmer accidently forgetting to pop item).
|
|
||||||
"""
|
|
||||||
|
|
||||||
__mHasPop: bool
|
|
||||||
__mAssocHierarchy: Hierarchy
|
|
||||||
|
|
||||||
def __init__(self, assoc_hierarchy: Hierarchy, item: str | int):
|
|
||||||
self.__mAssocHierarchy = assoc_hierarchy
|
|
||||||
self.__mHasPop = False
|
|
||||||
self.__mAssocHierarchy.push(item)
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_value, traceback):
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
def close(self) -> None:
|
|
||||||
if not self.__mHasPop:
|
|
||||||
self.__mAssocHierarchy.pop()
|
|
||||||
self.__mHasPop = True
|
|
||||||
|
|
||||||
def emplace(self, new_item: str | int) -> None:
|
|
||||||
"""
|
|
||||||
@brief Replace the content of top item in-place.
|
|
||||||
@details
|
|
||||||
In some cases, caller need to replace the content of top item.
|
|
||||||
For example, at the beginning, we only have index info.
|
|
||||||
After validating something, we can fetching a more human-readable info, such as name,
|
|
||||||
now we need replace the content of top item.
|
|
||||||
@param[in] new_item The new content of top item.
|
|
||||||
"""
|
|
||||||
self.__mAssocHierarchy.pop()
|
|
||||||
self.__mAssocHierarchy.push(new_item)
|
|
@ -1,51 +1,53 @@
|
|||||||
import logging
|
import logging, os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import common
|
import common
|
||||||
|
from common import AssetKind
|
||||||
import PIL, PIL.Image
|
import PIL, PIL.Image
|
||||||
|
|
||||||
# the config for thumbnail
|
# The HW size of thumbnail
|
||||||
THUMBNAIL_SIZE: int = 16
|
THUMBNAIL_SIZE: int = 16
|
||||||
|
|
||||||
class ThumbnailBuilder():
|
|
||||||
|
|
||||||
def __init__(self):
|
def _create_thumbnail(src_file: Path, dst_file: Path) -> None:
|
||||||
pass
|
# open image
|
||||||
|
src_image: PIL.Image.Image = PIL.Image.open(src_file)
|
||||||
|
# create thumbnail
|
||||||
|
src_image.thumbnail((THUMBNAIL_SIZE, THUMBNAIL_SIZE))
|
||||||
|
# save to new file
|
||||||
|
src_image.save(dst_file)
|
||||||
|
|
||||||
def build_thumbnails(self) -> None:
|
|
||||||
# get folder path
|
|
||||||
root_folder = common.get_plugin_folder()
|
|
||||||
|
|
||||||
# prepare handler
|
def build_icons() -> None:
|
||||||
def folder_handler(rel_name: str, src_folder: Path, dst_folder: Path) -> None:
|
raw_icons_dir = common.get_raw_assets_folder(AssetKind.Icons)
|
||||||
# just create folder
|
plg_icons_dir = common.get_plugin_assets_folder(AssetKind.Icons)
|
||||||
logging.info(f'Creating Folder: {src_folder} -> {dst_folder}')
|
|
||||||
dst_folder.mkdir(parents=False, exist_ok=True)
|
|
||||||
def file_handler(rel_name: str, src_file: Path, dst_file: Path) -> None:
|
|
||||||
# skip non-image
|
|
||||||
if src_file.suffix != '.png': return
|
|
||||||
# call thumbnail func
|
|
||||||
logging.info(f'Building Thumbnail: {src_file} -> {dst_file}')
|
|
||||||
self.__resize_image(src_file, dst_file)
|
|
||||||
|
|
||||||
# call common processor
|
# TODO: If we have Python 3.12, use Path.walk instead of current polyfill.
|
||||||
common.common_file_migrator(
|
|
||||||
root_folder / 'raw_icons',
|
|
||||||
root_folder / 'icons',
|
|
||||||
folder_handler,
|
|
||||||
file_handler
|
|
||||||
)
|
|
||||||
|
|
||||||
logging.info('Building thumbnail done.')
|
# Icon assets has subdirectory, so we need use another way to process.
|
||||||
|
for root, dirs, files in os.walk(raw_icons_dir):
|
||||||
|
root = Path(root)
|
||||||
|
|
||||||
|
# Iterate folders
|
||||||
|
for name in dirs:
|
||||||
|
# Fetch directory path
|
||||||
|
raw_icon_subdir = root / name
|
||||||
|
plg_icon_subdir = plg_icons_dir / raw_icon_subdir.relative_to(raw_icons_dir)
|
||||||
|
# Show message
|
||||||
|
logging.info(f'Creating Folder: {raw_icon_subdir} -> {plg_icon_subdir}')
|
||||||
|
# Create directory
|
||||||
|
plg_icon_subdir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Iterate files
|
||||||
|
for name in files:
|
||||||
|
# Fetch file path
|
||||||
|
raw_icon_file = root / name
|
||||||
|
plg_icon_file = plg_icons_dir / raw_icon_file.relative_to(raw_icons_dir)
|
||||||
|
# Show message
|
||||||
|
logging.info(f'Building Thumbnail: {raw_icon_file} -> {plg_icon_file}')
|
||||||
|
# Create thumbnail
|
||||||
|
_create_thumbnail(raw_icon_file, plg_icon_file)
|
||||||
|
|
||||||
def __resize_image(self, src_file: Path, dst_file: Path) -> None:
|
|
||||||
# open image
|
|
||||||
src_image: PIL.Image.Image = PIL.Image.open(src_file)
|
|
||||||
# create thumbnail
|
|
||||||
src_image.thumbnail((THUMBNAIL_SIZE, THUMBNAIL_SIZE))
|
|
||||||
# save to new file
|
|
||||||
src_image.save(dst_file)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
common.setup_logging()
|
common.setup_logging()
|
||||||
thumbnail_builder = ThumbnailBuilder()
|
build_icons()
|
||||||
thumbnail_builder.build_thumbnails()
|
|
||||||
|
@ -1,84 +1,46 @@
|
|||||||
import os, json, typing
|
import json, logging
|
||||||
import bme_utils, bme_relatives, simple_po
|
from pathlib import Path
|
||||||
import common
|
import common
|
||||||
|
from common import AssetKind
|
||||||
|
import json5
|
||||||
|
|
||||||
class JsonCompressor():
|
|
||||||
|
|
||||||
__mReporter: bme_utils.Reporter
|
def _compress_json(src_file: Path, dst_file: Path) -> None:
|
||||||
__mPoWriter: simple_po.PoWriter
|
# load data first
|
||||||
# __mValidator: bme_relatives.BMEValidator
|
with open(src_file, 'r', encoding='utf-8') as f:
|
||||||
__mExtractor: bme_relatives.BMEExtractor
|
loaded_prototypes = json5.load(f)
|
||||||
|
|
||||||
def __init__(self):
|
# save result with compress config
|
||||||
self.__mReporter = bme_utils.Reporter()
|
with open(dst_file, 'w', encoding='utf-8') as f:
|
||||||
self.__mPoWriter = simple_po.PoWriter(
|
json.dump(
|
||||||
os.path.join(common.get_plugin_folder(), 'i18n', 'bme.pot'),
|
loaded_prototypes, # loaded data
|
||||||
'BME Prototypes'
|
f,
|
||||||
)
|
indent=None, # no indent. the most narrow style.
|
||||||
# self.__mValidator = bme_relatives.BMEValidator(self.__mReporter)
|
separators=(',', ':'), # also for narrow style.
|
||||||
self.__mExtractor = bme_relatives.BMEExtractor(self.__mReporter, self.__mPoWriter)
|
sort_keys=False, # do not sort key
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_value, traceback):
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
def close(self) -> None:
|
|
||||||
self.__mPoWriter.close()
|
|
||||||
|
|
||||||
def run(self) -> None:
|
|
||||||
self.__compress_jsons()
|
|
||||||
|
|
||||||
def __compress_jsons(self) -> None:
|
|
||||||
# get folder path
|
|
||||||
root_folder: str = common.get_plugin_folder()
|
|
||||||
|
|
||||||
# prepare handler
|
|
||||||
def folder_handler(rel_name: str, src_folder: str, dst_folder: str) -> None:
|
|
||||||
# just create folder
|
|
||||||
self.__mReporter.info(f'Creating Folder: {src_folder} -> {dst_folder}')
|
|
||||||
os.makedirs(dst_folder, exist_ok = True)
|
|
||||||
def file_handler(rel_name: str, src_file: str, dst_file: str) -> None:
|
|
||||||
# skip non-json
|
|
||||||
if not src_file.endswith('.json'): return
|
|
||||||
# call compress func
|
|
||||||
self.__mReporter.info(f'Processing JSON: {src_file} -> {dst_file}')
|
|
||||||
self.__compress_json(rel_name, src_file, dst_file)
|
|
||||||
|
|
||||||
# call common processor
|
|
||||||
common.common_file_migrator(
|
|
||||||
os.path.join(root_folder, 'raw_jsons'),
|
|
||||||
os.path.join(root_folder, 'jsons'),
|
|
||||||
folder_handler,
|
|
||||||
file_handler
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.__mReporter.info('Building JSON done.')
|
|
||||||
|
|
||||||
def __compress_json(self, rel_name: str, src_file: str, dst_file: str) -> None:
|
def build_jsons() -> None:
|
||||||
# load data first
|
raw_jsons_dir = common.get_raw_assets_folder(AssetKind.Jsons)
|
||||||
loaded_prototypes: typing.Any
|
plg_jsons_dir = common.get_plugin_assets_folder(AssetKind.Jsons)
|
||||||
with open(src_file, 'r', encoding = 'utf-8') as fr:
|
|
||||||
loaded_prototypes = json.load(fr)
|
|
||||||
|
|
||||||
# validate loaded data
|
for raw_json_file in raw_jsons_dir.glob('*.json5'):
|
||||||
# self.__mValidator.validate(rel_name, loaded_prototypes)
|
# Skip non-file.
|
||||||
|
if not raw_json_file.is_file():
|
||||||
|
continue
|
||||||
|
|
||||||
# extract translation
|
# Build final path
|
||||||
self.__mExtractor.extract(rel_name, loaded_prototypes)
|
plg_json_file = plg_jsons_dir / raw_json_file.relative_to(raw_jsons_dir)
|
||||||
|
plg_json_file = plg_json_file.with_suffix('.json')
|
||||||
|
|
||||||
# save result
|
# Show message
|
||||||
with open(dst_file, 'w', encoding = 'utf-8') as fw:
|
logging.info(f'Compressing {raw_json_file} -> {plg_json_file}')
|
||||||
json.dump(
|
|
||||||
loaded_prototypes, # loaded data
|
# Compress json
|
||||||
fw,
|
_compress_json(raw_json_file, plg_json_file)
|
||||||
indent = None, # no indent. the most narrow style.
|
|
||||||
separators = (',', ':'), # also for narrow style.
|
|
||||||
sort_keys = False, # do not sort key
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
with JsonCompressor() as json_compressor:
|
common.setup_logging()
|
||||||
json_compressor.run()
|
build_jsons()
|
||||||
|
27
scripts/build_meshes.py
Normal file
27
scripts/build_meshes.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import shutil, logging
|
||||||
|
import common
|
||||||
|
from common import AssetKind
|
||||||
|
|
||||||
|
|
||||||
|
def build_meshes() -> None:
|
||||||
|
raw_meshes_dir = common.get_raw_assets_folder(AssetKind.Meshes)
|
||||||
|
plg_meshes_dir = common.get_plugin_assets_folder(AssetKind.Meshes)
|
||||||
|
|
||||||
|
for raw_ph_file in raw_meshes_dir.glob('*.ph'):
|
||||||
|
# Skip non-file.
|
||||||
|
if not raw_ph_file.is_file():
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Build final path
|
||||||
|
plg_ph_file = plg_meshes_dir / raw_ph_file.relative_to(raw_meshes_dir)
|
||||||
|
|
||||||
|
# Show message
|
||||||
|
logging.info(f'Copying {raw_ph_file} -> {plg_ph_file}')
|
||||||
|
|
||||||
|
# Copy placeholder
|
||||||
|
shutil.copyfile(raw_ph_file, plg_ph_file)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
common.setup_logging()
|
||||||
|
build_meshes()
|
@ -1,76 +1,39 @@
|
|||||||
import os, typing, logging
|
import logging, enum, typing
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
def get_plugin_folder() -> Path:
|
|
||||||
"""
|
|
||||||
Get the absolute path to plugin root folder.
|
|
||||||
|
|
||||||
:return: The absolute path to plugin root folder.
|
def get_root_folder() -> Path:
|
||||||
|
"""
|
||||||
|
Get the path to the root folder of this repository.
|
||||||
|
|
||||||
|
:return: The absolute path to the root folder of this repository.
|
||||||
"""
|
"""
|
||||||
return Path(__file__).resolve().parent.parent
|
return Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
def relative_to_folder(abs_path: Path, src_parent: Path, dst_parent: Path) -> Path:
|
|
||||||
|
class AssetKind(enum.StrEnum):
|
||||||
|
Icons = 'icons'
|
||||||
|
Jsons = 'jsons'
|
||||||
|
Meshes = 'meshes'
|
||||||
|
|
||||||
|
|
||||||
|
def get_raw_assets_folder(kind: AssetKind) -> Path:
|
||||||
"""
|
"""
|
||||||
Rebase one path to another path.
|
Get the path to the raw assets folder of given kind.
|
||||||
|
|
||||||
Give a absolute file path and folder path, and compute the relative path of given file to given folder.
|
:return: The absolute path to the raw assets folder of given kind.
|
||||||
Then applied the computed relative path to another given folder path.
|
|
||||||
Thus it seems like the file was rebased to from a folder to another folder with keeping the folder hierarchy.
|
|
||||||
|
|
||||||
For example, given `/path/to/file` and `/path`, it will compute relative path `to/file`.
|
|
||||||
Then it was applied to another folder path `/new` and got `/new/to/file`.
|
|
||||||
|
|
||||||
:param abs_path: The absolute path to a folder or file.
|
|
||||||
:param src_parent: The absolute path to folder which the `abs_path` will have relative path to.
|
|
||||||
:param dst_parent: The absolute path to folder which the relative path will be applied to.
|
|
||||||
"""
|
"""
|
||||||
return dst_parent / (abs_path.relative_to(src_parent))
|
return get_root_folder() / 'assets' / str(kind)
|
||||||
|
|
||||||
def common_file_migrator(
|
|
||||||
from_folder: Path, to_folder: Path,
|
def get_plugin_assets_folder(kind: AssetKind) -> Path:
|
||||||
fct_proc_folder: typing.Callable[[str, Path, Path], None],
|
|
||||||
fct_proc_file: typing.Callable[[str, Path, Path], None]) -> None:
|
|
||||||
"""
|
"""
|
||||||
Common file migrator used by some build script.
|
Get the path to the plugin assets folder of given kind.
|
||||||
|
|
||||||
This function receive 2 absolute folder path. `from_folder` indicate the file migrated out,
|
:return: The absolute path to the plugin assets folder of given kind.
|
||||||
and `to_folder` indicate the file migrated in.
|
|
||||||
`fct_proc_folder` is a function pointer from caller which handle folder migration in detail.
|
|
||||||
`fct_proc_file` is same but handle file migration.
|
|
||||||
|
|
||||||
`fct_proc_folder` will receive 3 args.
|
|
||||||
First is the name of this folder which can be shown for end user.
|
|
||||||
Second is the source folder and third is expected dest folder.
|
|
||||||
`fct_proc_file` is same, but receive the file path instead.
|
|
||||||
Both of these function pointer should do the migration in detail. This function will only just iterate
|
|
||||||
folder and give essential args and will not do any migration operations such as copying or moving.
|
|
||||||
|
|
||||||
:param from_folder: The folder need to be migrated.
|
|
||||||
:param to_folder: The folder will be migrated to.
|
|
||||||
:param fct_proc_folder: Folder migration detail handler.
|
|
||||||
:param fct_proc_file: File migration detail handler.
|
|
||||||
"""
|
"""
|
||||||
# TODO: If we have Python 3.12, use Path.walk instead of current polyfill.
|
return get_root_folder() / 'bbp_ng' / str(kind)
|
||||||
|
|
||||||
# iterate from_folder folder
|
|
||||||
for root, dirs, files in os.walk(from_folder, topdown=True):
|
|
||||||
root = Path(root)
|
|
||||||
|
|
||||||
# iterate folders
|
|
||||||
for name in dirs:
|
|
||||||
# prepare handler args
|
|
||||||
src_folder = root / name
|
|
||||||
dst_folder = relative_to_folder(src_folder, from_folder, to_folder)
|
|
||||||
# call handler
|
|
||||||
fct_proc_folder(name, src_folder, dst_folder)
|
|
||||||
|
|
||||||
# iterate files
|
|
||||||
for name in files:
|
|
||||||
# prepare handler args
|
|
||||||
src_file = root / name
|
|
||||||
dst_file = relative_to_folder(src_file, from_folder, to_folder)
|
|
||||||
# call handler
|
|
||||||
fct_proc_file(name, src_file, dst_file)
|
|
||||||
|
|
||||||
def setup_logging() -> None:
|
def setup_logging() -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
import common
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
common.setup_logging()
|
|
104
scripts/extract_jsons.py
Normal file
104
scripts/extract_jsons.py
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import logging, typing, itertools
|
||||||
|
from pathlib import Path
|
||||||
|
import common, bme
|
||||||
|
from common import AssetKind
|
||||||
|
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'
|
||||||
|
|
||||||
|
|
||||||
|
class JsonsExtractor:
|
||||||
|
|
||||||
|
po: polib.POFile
|
||||||
|
"""Extracted PO file"""
|
||||||
|
categories: set[str]
|
||||||
|
"""Set for removing duplicated category names"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
# create po file
|
||||||
|
self.po = polib.POFile()
|
||||||
|
self.po.metadata = {
|
||||||
|
'Project-Id-Version': '1.0',
|
||||||
|
'Report-Msgid-Bugs-To': 'you@example.com',
|
||||||
|
'POT-Creation-Date': 'YEAR-MO-DA HO:MI+ZONE',
|
||||||
|
'PO-Revision-Date': 'YEAR-MO-DA HO:MI+ZONE',
|
||||||
|
'Last-Translator': 'FULL NAME <EMAIL@ADDRESS>',
|
||||||
|
'Language-Team': 'LANGUAGE <LL@li.org>',
|
||||||
|
'MIME-Version': '1.0',
|
||||||
|
'Content-Type': 'text/plain; charset=utf-8',
|
||||||
|
'Content-Transfer-Encoding': '8bit',
|
||||||
|
'X-Generator': 'polib',
|
||||||
|
}
|
||||||
|
# create category set
|
||||||
|
self.categories = set()
|
||||||
|
|
||||||
|
def __extract_prototype(self, prototype: bme.Prototype) -> None:
|
||||||
|
identifier = prototype.identifier
|
||||||
|
showcase = prototype.showcase
|
||||||
|
|
||||||
|
# Show message
|
||||||
|
logging.info(f'Extracting prototype {identifier}')
|
||||||
|
|
||||||
|
# Extract showcase
|
||||||
|
if showcase is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Extract showcase title
|
||||||
|
self.po.append(polib.POEntry(msgid=showcase.title, msgstr='', msgctxt=f'{CTX_PROTOTYPE}/{identifier}'))
|
||||||
|
# extract showcase category
|
||||||
|
if showcase.category not in self.categories:
|
||||||
|
self.po.append(polib.POEntry(msgid=showcase.category, msgstr='', msgctxt=CTX_CATEGORY))
|
||||||
|
self.categories.add(showcase.category)
|
||||||
|
# Extract showcase entries
|
||||||
|
for i, cfg in enumerate(showcase.cfgs):
|
||||||
|
# extract title and description
|
||||||
|
self.po.append(polib.POEntry(msgid=cfg.title, msgstr='', msgctxt=f'{CTX_PROTOTYPE}/{identifier}/[{i}]'))
|
||||||
|
self.po.append(polib.POEntry(msgid=cfg.desc, msgstr='', msgctxt=f'{CTX_PROTOTYPE}/{identifier}/[{i}]'))
|
||||||
|
|
||||||
|
def __extract_json(self, json_file: Path) -> None:
|
||||||
|
# Show message
|
||||||
|
logging.info(f'Extracting file {json_file}')
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Read file and convert it into BME struct.
|
||||||
|
with open(json_file, 'r', encoding='utf-8') as f:
|
||||||
|
document = json5.load(f)
|
||||||
|
prototypes = bme.Prototypes.model_validate(document)
|
||||||
|
# Extract translation
|
||||||
|
for prototype in prototypes.root:
|
||||||
|
self.__extract_prototype(prototype)
|
||||||
|
except pydantic.ValidationError:
|
||||||
|
logging.error(
|
||||||
|
f'Can not extract translation from {json_file} due to struct error. Please validate it first.')
|
||||||
|
except (ValueError, UnicodeDecodeError):
|
||||||
|
logging.error(f'Can not extract translation from {json_file} due to JSON5 error. Please validate it first.')
|
||||||
|
|
||||||
|
def extract_jsons(self) -> None:
|
||||||
|
raw_jsons_dir = common.get_raw_assets_folder(AssetKind.Jsons)
|
||||||
|
|
||||||
|
# Iterate all prototypes and add into POT
|
||||||
|
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)
|
||||||
|
|
||||||
|
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()
|
||||||
|
extractor = JsonsExtractor()
|
||||||
|
extractor.extract_jsons()
|
||||||
|
extractor.save()
|
@ -3,6 +3,7 @@ name = "scripts"
|
|||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"json5>=0.12.0",
|
||||||
"pillow==10.2.0",
|
"pillow==10.2.0",
|
||||||
"polib>=1.2.0",
|
"polib>=1.2.0",
|
||||||
"pydantic>=2.11.7",
|
"pydantic>=2.11.7",
|
||||||
|
@ -1,99 +0,0 @@
|
|||||||
import typing
|
|
||||||
import io
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
class PoWriter():
|
|
||||||
"""
|
|
||||||
The simple PO file writer.
|
|
||||||
This class is just served for writing POT files.
|
|
||||||
It may be convenient when exporting PO file for thoese whose format can not be parsed by formal tools.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__cEscapeCharsDict: typing.ClassVar[dict[str, str]] = {
|
|
||||||
'\\': '\\\\',
|
|
||||||
'"': '\\"',
|
|
||||||
'\n': '\\n',
|
|
||||||
'\t': '\\t',
|
|
||||||
}
|
|
||||||
__cEscapeCharsTable: typing.ClassVar[dict] = str.maketrans(__cEscapeCharsDict)
|
|
||||||
__mPoFile: io.TextIOWrapper
|
|
||||||
|
|
||||||
def __init__(self, po_file_path: str, project_name: str):
|
|
||||||
# open file
|
|
||||||
self.__mPoFile = open(po_file_path, 'w', encoding = 'utf-8')
|
|
||||||
# add default header
|
|
||||||
self.__add_header(project_name)
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_value, traceback):
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
def close(self) -> None:
|
|
||||||
self.__mPoFile.close()
|
|
||||||
|
|
||||||
def __write_line(self, val: str) -> None:
|
|
||||||
self.__mPoFile.write(val)
|
|
||||||
self.__mPoFile.write('\n')
|
|
||||||
|
|
||||||
def __escape_str(self, val: str) -> str:
|
|
||||||
"""
|
|
||||||
This function escapes a given string to make it safe to use as a C++ string literal.
|
|
||||||
@param[in] val Original string
|
|
||||||
@return Escaped string
|
|
||||||
"""
|
|
||||||
return val.translate(PoWriter.__cEscapeCharsTable)
|
|
||||||
|
|
||||||
def __add_header(self, project_name: str) -> None:
|
|
||||||
"""
|
|
||||||
Add default header for PO file.
|
|
||||||
@param[in] project_name The project name written in file.
|
|
||||||
"""
|
|
||||||
now_datetime = datetime.datetime.now()
|
|
||||||
self.__write_line('# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.')
|
|
||||||
self.__write_line('msgid ""')
|
|
||||||
self.__write_line('msgstr ""')
|
|
||||||
self.__write_line(f'"Project-Id-Version: {self.__escape_str(project_name)}\\n"')
|
|
||||||
self.__write_line(f'"POT-Creation-Date: {now_datetime.strftime("%Y-%m-%d %H:%M%Z")}\\n"')
|
|
||||||
self.__write_line('"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n"')
|
|
||||||
self.__write_line('"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n"')
|
|
||||||
self.__write_line('"Language-Team: LANGUAGE <LL@li.org>\\n"')
|
|
||||||
self.__write_line('"Language: __POT__\\n"')
|
|
||||||
self.__write_line('"MIME-Version: 1.0\\n"')
|
|
||||||
self.__write_line('"Content-Type: text/plain; charset=UTF-8\\n"')
|
|
||||||
self.__write_line('"Content-Transfer-Encoding: 8bit\\n"')
|
|
||||||
self.__write_line('"X-Generator: simple_po.PoWriter\\n"')
|
|
||||||
|
|
||||||
def add_entry(self, msg: str, msg_context: str | None = None, extracted_comment: str | None = None, reference: str | None = None) -> None:
|
|
||||||
"""
|
|
||||||
@brief Write an entry into PO file with given arguments.
|
|
||||||
@details
|
|
||||||
Please note this function will NOT check whether there already is a duplicated entry which has been written.
|
|
||||||
You must check this on your own.
|
|
||||||
@param[in] msg The message string need to be translated.
|
|
||||||
@param[in] msg_context The context of this message.
|
|
||||||
@param[in] extracted_comment The extracted comment of this message. None if no reference. Line breaker is not allowed.
|
|
||||||
@param[in] reference The code refernece of this message. None if no reference. Line breaker is not allowed.
|
|
||||||
"""
|
|
||||||
# empty string will not be translated
|
|
||||||
if msg == '': return
|
|
||||||
|
|
||||||
# write blank line first
|
|
||||||
self.__write_line('')
|
|
||||||
if extracted_comment:
|
|
||||||
self.__write_line(f'#. {extracted_comment}')
|
|
||||||
if reference:
|
|
||||||
self.__write_line(f'#: {reference}')
|
|
||||||
if msg_context:
|
|
||||||
self.__write_line(f'msgctxt "{self.__escape_str(msg_context)}"')
|
|
||||||
self.__write_line(f'msgid "{self.__escape_str(msg)}"')
|
|
||||||
self.__write_line('msgstr ""')
|
|
||||||
|
|
||||||
def build_code_reference(self, code_file_path: str, code_line_number: int) -> str:
|
|
||||||
"""
|
|
||||||
A convenient function to build code reference string used when adding entry.
|
|
||||||
@param[in] code_file_path The path to associated code file.
|
|
||||||
@param[in] code_line_number The line number of associated code within given file.
|
|
||||||
"""
|
|
||||||
return f'{code_file_path}:{code_line_number}'
|
|
13
scripts/uv.lock
generated
13
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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "pillow"
|
name = "pillow"
|
||||||
version = "10.2.0"
|
version = "10.2.0"
|
||||||
@ -131,10 +140,11 @@ wheels = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tools"
|
name = "scripts"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
{ name = "json5" },
|
||||||
{ name = "pillow" },
|
{ name = "pillow" },
|
||||||
{ name = "polib" },
|
{ name = "polib" },
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
@ -142,6 +152,7 @@ dependencies = [
|
|||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
|
{ name = "json5", specifier = ">=0.12.0" },
|
||||||
{ name = "pillow", specifier = "==10.2.0" },
|
{ name = "pillow", specifier = "==10.2.0" },
|
||||||
{ name = "polib", specifier = ">=1.2.0" },
|
{ name = "polib", specifier = ">=1.2.0" },
|
||||||
{ name = "pydantic", specifier = ">=2.11.7" },
|
{ name = "pydantic", specifier = ">=2.11.7" },
|
||||||
|
@ -1,175 +0,0 @@
|
|||||||
import enum
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import ast
|
|
||||||
from typing import Optional, Self
|
|
||||||
from pydantic import BaseModel, RootModel, Field, model_validator, ValidationError
|
|
||||||
import common
|
|
||||||
|
|
||||||
|
|
||||||
def validate_programmable_str(probe: str) -> None:
|
|
||||||
try:
|
|
||||||
ast.parse(probe)
|
|
||||||
except SyntaxError:
|
|
||||||
raise ValueError(
|
|
||||||
f'String {probe} may not be a valid Python statement which is not suit for programmable field.')
|
|
||||||
|
|
||||||
|
|
||||||
class ShowcaseType(enum.StrEnum):
|
|
||||||
Nothing = 'none'
|
|
||||||
Floor = 'floor'
|
|
||||||
Rail = 'Rail'
|
|
||||||
Wood = 'wood'
|
|
||||||
|
|
||||||
|
|
||||||
class ShowcaseCfgType(enum.StrEnum):
|
|
||||||
Float = 'float'
|
|
||||||
Int = 'int'
|
|
||||||
Bool = 'bool'
|
|
||||||
Face = 'face'
|
|
||||||
|
|
||||||
|
|
||||||
class ShowcaseCfg(BaseModel):
|
|
||||||
field: str = Field(frozen=True, strict=True)
|
|
||||||
type: ShowcaseCfgType = Field(frozen=True)
|
|
||||||
title: str = Field(frozen=True, strict=True)
|
|
||||||
desc: str = Field(frozen=True, strict=True)
|
|
||||||
default: str = Field(frozen=True, strict=True)
|
|
||||||
|
|
||||||
@model_validator(mode='after')
|
|
||||||
def verify_prog_field(self) -> Self:
|
|
||||||
validate_programmable_str(self.default)
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
class Showcase(BaseModel):
|
|
||||||
title: 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)
|
|
||||||
|
|
||||||
|
|
||||||
class Param(BaseModel):
|
|
||||||
field: str = Field(frozen=True, strict=True)
|
|
||||||
data: str = Field(frozen=True, strict=True)
|
|
||||||
|
|
||||||
@model_validator(mode='after')
|
|
||||||
def verify_prog_field(self) -> Self:
|
|
||||||
validate_programmable_str(self.data)
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
class Var(BaseModel):
|
|
||||||
field: str = Field(frozen=True, strict=True)
|
|
||||||
data: str = Field(frozen=True, strict=True)
|
|
||||||
|
|
||||||
@model_validator(mode='after')
|
|
||||||
def verify_prog_field(self) -> Self:
|
|
||||||
validate_programmable_str(self.data)
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
class Vertex(BaseModel):
|
|
||||||
skip: str = Field(frozen=True, strict=True)
|
|
||||||
data: str = Field(frozen=True, strict=True)
|
|
||||||
|
|
||||||
@model_validator(mode='after')
|
|
||||||
def verify_prog_field(self) -> Self:
|
|
||||||
validate_programmable_str(self.skip)
|
|
||||||
validate_programmable_str(self.data)
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
class Face(BaseModel):
|
|
||||||
skip: str = Field(frozen=True, strict=True)
|
|
||||||
texture: str = Field(frozen=True, strict=True)
|
|
||||||
indices: list[int] = Field(frozen=True, strict=True)
|
|
||||||
uvs: list[str] = Field(frozen=True, strict=True)
|
|
||||||
normals: Optional[list[str]] = Field(frozen=True, strict=True)
|
|
||||||
|
|
||||||
@model_validator(mode='after')
|
|
||||||
def verify_count(self) -> Self:
|
|
||||||
expected_count = len(self.indices)
|
|
||||||
if len(self.uvs) != expected_count:
|
|
||||||
raise ValueError('The length of uv array is not matched with indices.')
|
|
||||||
if (self.normals is not None) and (len(self.normals) != expected_count):
|
|
||||||
raise ValueError('The length of normal array is not matched with indices.')
|
|
||||||
return self
|
|
||||||
|
|
||||||
@model_validator(mode='after')
|
|
||||||
def verify_prog_field(self) -> Self:
|
|
||||||
validate_programmable_str(self.skip)
|
|
||||||
validate_programmable_str(self.texture)
|
|
||||||
for i in self.uvs:
|
|
||||||
validate_programmable_str(i)
|
|
||||||
if self.normals is not None:
|
|
||||||
for i in self.normals:
|
|
||||||
validate_programmable_str(i)
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
class Instance(BaseModel):
|
|
||||||
identifier: str = Field(frozen=True, strict=True)
|
|
||||||
skip: str = Field(frozen=True, strict=True)
|
|
||||||
params: dict[str, str] = Field(frozen=True, strict=True)
|
|
||||||
transform: str = Field(frozen=True, strict=True)
|
|
||||||
|
|
||||||
@model_validator(mode='after')
|
|
||||||
def verify_prog_field(self) -> Self:
|
|
||||||
validate_programmable_str(self.skip)
|
|
||||||
for v in self.params.values():
|
|
||||||
validate_programmable_str(v)
|
|
||||||
validate_programmable_str(self.transform)
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
IDENTIFIERS: set[str] = set()
|
|
||||||
|
|
||||||
|
|
||||||
class Prototype(BaseModel):
|
|
||||||
identifier: str = Field(frozen=True, strict=True)
|
|
||||||
showcase: Optional[Showcase] = Field(frozen=True, strict=True)
|
|
||||||
params: list[Param] = Field(frozen=True, strict=True)
|
|
||||||
skip: str = Field(frozen=True, strict=True)
|
|
||||||
vars: list[Var] = Field(frozen=True, strict=True)
|
|
||||||
vertices: list[Vertex] = Field(frozen=True, strict=True)
|
|
||||||
faces: list[Face] = Field(frozen=True, strict=True)
|
|
||||||
instances: list[Instance] = Field(frozen=True, strict=True)
|
|
||||||
|
|
||||||
@model_validator(mode='after')
|
|
||||||
def verify_identifier(self) -> Self:
|
|
||||||
global IDENTIFIERS
|
|
||||||
if self.identifier in IDENTIFIERS:
|
|
||||||
raise ValueError(f'Identifier {self.identifier} is already registered.')
|
|
||||||
else:
|
|
||||||
IDENTIFIERS.add(self.identifier)
|
|
||||||
return self
|
|
||||||
|
|
||||||
@model_validator(mode='after')
|
|
||||||
def verify_prog_field(self) -> Self:
|
|
||||||
validate_programmable_str(self.skip)
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
class Prototypes(RootModel):
|
|
||||||
root: list[Prototype] = Field(frozen=True, strict=True)
|
|
||||||
|
|
||||||
|
|
||||||
def validate_json() -> None:
|
|
||||||
raw_json_folder = common.get_plugin_folder() / 'raw_jsons'
|
|
||||||
|
|
||||||
for json_file in raw_json_folder.rglob('*.json'):
|
|
||||||
logging.info(f'Validating {json_file} ...')
|
|
||||||
try:
|
|
||||||
with open(json_file, 'r', encoding='utf-8') as f:
|
|
||||||
docuement = json.load(f)
|
|
||||||
Prototypes.model_validate(docuement)
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
logging.error(f'Can not load file {json_file}. It may not a valid JSON file. Reason: {e}')
|
|
||||||
except ValidationError as e:
|
|
||||||
logging.error(f'File {json_file} is not correct. Reason: {e}')
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
common.setup_logging()
|
|
||||||
validate_json()
|
|
207
scripts/validate_jsons.py
Normal file
207
scripts/validate_jsons.py
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
import logging, ast, typing
|
||||||
|
import common, bme
|
||||||
|
from common import AssetKind
|
||||||
|
import pydantic, json5
|
||||||
|
|
||||||
|
#region Assistant Checker
|
||||||
|
|
||||||
|
|
||||||
|
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 _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'Showcase icon value {icon_name} may be invalid.')
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Core Validator
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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('*.json5'):
|
||||||
|
# Skip non-file
|
||||||
|
if not raw_json_file.is_file():
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Show info
|
||||||
|
logging.info(f'Loading {raw_json_file}')
|
||||||
|
|
||||||
|
# Load prototypes
|
||||||
|
try:
|
||||||
|
with open(raw_json_file, 'r', encoding='utf-8') as f:
|
||||||
|
docuement = json5.load(f)
|
||||||
|
file_prototypes = bme.Prototypes.model_validate(docuement)
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
_pre_validate_prototype(prototype, identifiers)
|
||||||
|
|
||||||
|
# Start custom validation
|
||||||
|
for prototype in prototypes:
|
||||||
|
_validate_prototype(prototype, identifiers)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
common.setup_logging()
|
||||||
|
validate_jsons()
|
Reference in New Issue
Block a user