18 Commits

Author SHA1 Message Date
427bad4f6b fix: fix various trivial issues
- update blender_manifest.toml to the latest version and fix the issues raised by blender when packaging.
- use blender do packaging work. remove redist.py because blender_manifest.toml has gitignore like filter feature when packaging.
- update document about installing, configurating, building plugin for blender 4.2.
- update a document image for blender 4.2.
2024-07-19 15:25:38 +08:00
209d212287 fix: fix blender mesh writer issue.
- fix blender mesh writer with the reference of FBX Importer.
- ballance map importer now may works as expected (still need more review).
2024-07-18 20:23:21 +08:00
2271b0a621 fix: remove bl_info.
- remove bl_info in __init__.py because blender_manifest.toml has replace it.
2024-07-17 21:20:40 +08:00
6940428b88 fix: fix blender 4.2 mesh issue.
- fix issue that blender 4.2 lost use_auto_smooth and calc_split_normals. (it almost works but not perfect. still need more debugging)
- remove depracted material blend_mode. use modern one instead. (the modern setting make me consufes but it works)
2024-07-17 19:45:35 +08:00
aa602a7bb8 fix: update to blender 4.2 LTS
- update to Blender 4.2 LTS
- fix issue that can not find specular property in priniciple BSDF when applying virtools material.
- update blender manifest toml to let plugin works.
- there are various mesh works need to be fixed in later commits. this commit can not works normally on blender 4.2
2024-07-17 09:23:16 +08:00
8588f097a2 fix: change default screw steps to 28.
- change default screw steps to 28 (7 steps for 90 degree)
- update blender mesh annotation.
2024-05-25 20:32:30 +08:00
5d8ffb7e48 feat: add redist script
- add redist script for the convenience of building plugin package, especially for Blender 4.2
2024-05-21 16:19:13 +08:00
270fddff52 feat: do some trivial adaptions for blender 4.2 LTS
- add blender_manifest.toml
- add a removal note for bl_info in __init__.py.
- it doesn't mean that this plugin now can be used in blender 4.2. I just add some trivial changes which do not break current compatibility to reduce migration work in future.
2024-05-17 09:15:12 +08:00
084e7fbe61 feat: add extra transform for rail creation
- add extra transform for rail creation.
- remove scale from extra transform because it is rarely used. because Ballance can not handle the physicalization of scaled object.
2024-05-13 13:39:42 +08:00
190be6ec61 fix: restore extra transform to identy when entering bme operator. 2024-05-10 15:33:28 +08:00
36e925101e feat: extra transform properties for bme creation.
- add extra transform properties (translation, rotation and scale) for bme creation operator. this is good for "what you seen is what you gotten" and can increase user experience when add bme protorypes. please note these extra transform is applied to object after moving to cursor, not the mesh self.
- fix a weired bug that bme creation execute() may get outdated configurations when switch bme type in pop up window. the solution is simply adding an extra outdated flag checker at the head of execute(). but i have no spare time to research whether this patch is correct for this bug.
2024-05-10 14:44:37 +08:00
84e7e8380f feat: hide some operators in edit mode and add some notify for grouping oper.
- now grouping, ungrouping clear groups operators will show a report at the bottom of blender to indicate their work have done.
- disable fix material, 3ds max align, select by virtools group operators in edit mode to prevent any possible undefined behavior.
2024-04-22 21:06:41 +08:00
c58af8ce48 fix: fix face generation error of BME streets prototype. 2024-04-22 21:06:35 +08:00
07298fd21c feat: add blc params in exporter. fix bme.
- add ballance params section and successive switch in virtools file exporter to allow user manually cancel sector group ensurance which give user a way to export component, not the whole level.
- add mirror matrix detection in bme creation. a mirror matrix will reverse triangle indice order. we need reverse it again when facing mirror matrix. this resolves the issue when applying term 3.
- use mirror, not rotation to create straight floor in bme which give a better looks in game.
2024-04-07 10:35:06 +08:00
3a1a0fb0f6 doc: update document
- add section about ballance related props
2024-04-06 15:43:42 +08:00
3396947115 feat: add ballance map sector field in scene
- add ballance map sector info in scene to indicate the maximum sector count of this map.
- this adding will prevent the bug that the exported ballance map do not have successive sector groups. because original implement will not create sector group if no component in corresponding sector and previous remedy still have bug. and if this happended, ballance will show spaceship in wrong sector. this adding is the final solution of this bug.
- exlarge ballance map sector info when user adding component. the enlarged value will be calculated by user input sector.
- auto enlarge ballance map sector info when importing. this will give user a fluent experience when modifying existing map.
- exporting map will also use ballance map sector info to pre-create successive sector group as term 2 stated.
- move sector name extractor from virtools file exporting module to naming convention module.
2024-04-01 14:39:11 +08:00
6cf2ab895d fix: fix unregister error in virtools group
- fix del attribute error when unregister virtools group.
2024-03-30 16:12:34 +08:00
b039dd8b43 fix: fix grouping and naming error
- fix wood and rail soundeffect grouping error.
- fix checkpoint and resetpoint naming error again (forget to change PC/PR).
2024-02-20 21:52:50 +08:00
36 changed files with 635 additions and 199 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
# disable distribution build folder # disable distribution build folder
redist/ redist/
!redist/.gitkeep

View File

@ -1,4 +1,4 @@
import bpy import bpy, mathutils
import typing import typing
from . import PROP_preferences from . import PROP_preferences
from . import UTIL_functions, UTIL_bme from . import UTIL_functions, UTIL_bme
@ -43,7 +43,7 @@ class BBP_OT_add_bme_struct(bpy.types.Operator):
# So these is the solution about generating cache list according to the change of bme struct type. # 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. # First, update function will only set a "outdated" flag for operator which is a pre-registered Blender property.
# The "outdated" flags is not showen and not saved. # The "outdated" flags is not showen and not saved.
# Then call a internal cache list update function at the begin of `invoke` and `draw`. # Then call a internal cache list update function at the begin of `invoke`, `execute` and `draw`.
# In this internal cache list updator, check "outdated" flag first, if cache is outdated, update and reset flag. # In this internal cache list updator, check "outdated" flag first, if cache is outdated, update and reset flag.
# Otherwise do nothing. # Otherwise do nothing.
# #
@ -141,11 +141,34 @@ class BBP_OT_add_bme_struct(bpy.types.Operator):
type = BBP_PG_bme_adder_cfgs, type = BBP_PG_bme_adder_cfgs,
) # type: ignore ) # type: ignore
## Extra transform for good "what you see is what you gotten".
# Extra transform will be added after moving this object to cursor.
extra_translation: bpy.props.FloatVectorProperty(
name = "Extra Translation",
description = "The extra translation applied to object after moving to cursor.",
size = 3,
subtype = 'TRANSLATION',
step = 50, # same step as the float entry of BBP_PG_bme_adder_cfgs
default = (0.0, 0.0, 0.0)
) # type: ignore
extra_rotation: bpy.props.FloatVectorProperty(
name = "Extra Rotation",
description = "The extra rotation applied to object after moving to cursor.",
size = 3,
subtype = 'EULER',
step = 100, # We choosen 100, mean 1. Sync with property window.
default = (0.0, 0.0, 0.0)
) # type: ignore
@classmethod @classmethod
def poll(self, context): def poll(cls, context):
return PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder() return PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder()
def invoke(self, context, event): def invoke(self, context, event):
# reset extra transform to identy
self.extra_translation = (0.0, 0.0, 0.0)
self.extra_rotation = (0.0, 0.0, 0.0)
self.extra_scale = (1.0, 1.0, 1.0)
# create internal list # create internal list
self.bme_struct_cfg_index_cache = [] self.bme_struct_cfg_index_cache = []
# trigger default bme struct type updator # trigger default bme struct type updator
@ -156,6 +179,9 @@ class BBP_OT_add_bme_struct(bpy.types.Operator):
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()
# collect cfgs data # collect cfgs data
cfgs: dict[str, typing.Any] = {} cfgs: dict[str, typing.Any] = {}
for (cfg, cfg_index) in self.bme_struct_cfg_index_cache: for (cfg, cfg_index) in self.bme_struct_cfg_index_cache:
@ -178,8 +204,14 @@ class BBP_OT_add_bme_struct(bpy.types.Operator):
cfgs cfgs
) )
# move to cursor # add into scene and move to cursor
UTIL_functions.add_into_scene_and_move_to_cursor(obj) UTIL_functions.add_into_scene_and_move_to_cursor(obj)
# add extra transform
obj.matrix_world = obj.matrix_world @ mathutils.Matrix.LocRotScale(
mathutils.Vector(self.extra_translation),
mathutils.Euler(self.extra_rotation, 'XYZ'),
mathutils.Vector((1.0, 1.0, 1.0)) # no scale
)
# select created object # select created object
UTIL_functions.select_certain_objects((obj, )) UTIL_functions.select_certain_objects((obj, ))
return {'FINISHED'} return {'FINISHED'}
@ -194,6 +226,7 @@ class BBP_OT_add_bme_struct(bpy.types.Operator):
layout.prop(self, 'bme_struct_type') layout.prop(self, 'bme_struct_type')
# visit cfgs cache list to show cfg # visit cfgs cache list to show cfg
layout.label(text = "Prototype Configurations:")
for (cfg, cfg_index) in self.bme_struct_cfg_index_cache: for (cfg, cfg_index) in self.bme_struct_cfg_index_cache:
# create box for cfgs # create box for cfgs
box_layout: bpy.types.UILayout = layout.box() box_layout: bpy.types.UILayout = layout.box()
@ -225,12 +258,24 @@ class BBP_OT_add_bme_struct(bpy.types.Operator):
grids.prop(self.bme_struct_cfgs[cfg_index + 1], 'prop_bool', text = 'Bottom') # bottom grids.prop(self.bme_struct_cfgs[cfg_index + 1], 'prop_bool', text = 'Bottom') # bottom
grids.separator() grids.separator()
# show extra transform props
# forcely order that each one are placed horizontally
layout.label(text = "Extra Transform:")
# translation
layout.label(text = 'Translation')
hbox_layout: bpy.types.UILayout = layout.row()
hbox_layout.prop(self, 'extra_translation', text = '')
# rotation
layout.label(text = 'Rotation')
hbox_layout = layout.row()
hbox_layout.prop(self, 'extra_rotation', text = '')
@classmethod @classmethod
def draw_blc_menu(self, layout: bpy.types.UILayout): def draw_blc_menu(cls, layout: bpy.types.UILayout):
for ident in _g_EnumHelper_BmeStructType.get_bme_identifiers(): for ident in _g_EnumHelper_BmeStructType.get_bme_identifiers():
# draw operator # draw operator
cop = layout.operator( cop = layout.operator(
self.bl_idname, cls.bl_idname,
text = _g_EnumHelper_BmeStructType.get_bme_showcase_title(ident), text = _g_EnumHelper_BmeStructType.get_bme_showcase_title(ident),
icon_value = _g_EnumHelper_BmeStructType.get_bme_showcase_icon(ident) icon_value = _g_EnumHelper_BmeStructType.get_bme_showcase_icon(ident)
) )

View File

@ -1,7 +1,7 @@
import bpy, mathutils import bpy, mathutils
import math, typing import math, typing
from . import UTIL_functions, UTIL_icons_manager, UTIL_naming_convension from . import UTIL_functions, UTIL_icons_manager, UTIL_naming_convension
from . import PROP_ballance_element, PROP_virtools_group from . import PROP_ballance_element, PROP_virtools_group, PROP_ballance_map_info
#region Param Help Classes #region Param Help Classes
@ -128,6 +128,7 @@ class _GeneralComponentCreator():
""" """
# get element info first # get element info first
ele_info: UTIL_naming_convension.BallanceObjectInfo = _get_component_info(comp_type, comp_sector) ele_info: UTIL_naming_convension.BallanceObjectInfo = _get_component_info(comp_type, comp_sector)
# create blc element context # create blc element context
with PROP_ballance_element.BallanceElementsHelper(bpy.context.scene) as creator: with PROP_ballance_element.BallanceElementsHelper(bpy.context.scene) as creator:
# object creation counter # object creation counter
@ -143,6 +144,27 @@ class _GeneralComponentCreator():
# put into created object list # put into created object list
self.__mObjList.append(obj) self.__mObjList.append(obj)
# enlarge scene sector field for non-PS (start point) PE (end point) component
# read from scene and create var for enlarged sector count
map_info: PROP_ballance_map_info.RawBallanceMapInfo = PROP_ballance_map_info.get_raw_ballance_map_info(bpy.context.scene)
enlarged_sector: int
# check component type to get enlarged value
match(ele_info.mBasicType):
case UTIL_naming_convension.BallanceObjectType.COMPONENT:
enlarged_sector = comp_sector
case UTIL_naming_convension.BallanceObjectType.CHECKPOINT:
# checkpoint 1 means that there is sector 2, so we plus 1 for it.
enlarged_sector = comp_sector + 1
case UTIL_naming_convension.BallanceObjectType.RESETPOINT:
enlarged_sector = comp_sector
case _:
# this component is not a sector based component
# so we do not change it (use original value)
enlarged_sector = map_info.mSectorCount
# enlarge it
map_info.mSectorCount = max(map_info.mSectorCount, enlarged_sector)
PROP_ballance_map_info.set_raw_ballance_map_info(bpy.context.scene, map_info)
def finish_component(self) -> None: def finish_component(self) -> None:
""" """
Finish up component creation. Finish up component creation.

View File

@ -33,6 +33,50 @@ c_SideSpiralRailScrew: float = 3.6
#region Operator Helpers #region Operator Helpers
class SharedExtraTransform():
"""
This class is served for all rail creation which allow user
provide extra transform after moving created rail to cursor.
For "what you look is what you gotten" experience, this extra transform is essential.
"""
extra_translation: bpy.props.FloatVectorProperty(
name = "Extra Translation",
description = "The extra translation applied to object after moving to cursor.",
size = 3,
subtype = 'TRANSLATION',
step = 50, # same step as the float entry of BBP_PG_bme_adder_cfgs
default = (0.0, 0.0, 0.0)
) # type: ignore
extra_rotation: bpy.props.FloatVectorProperty(
name = "Extra Rotation",
description = "The extra rotation applied to object after moving to cursor.",
size = 3,
subtype = 'EULER',
step = 100, # We choosen 100, mean 1. Sync with property window.
default = (0.0, 0.0, 0.0)
) # type: ignore
def draw_extra_transform_input(self, layout: bpy.types.UILayout) -> None:
# show extra transform props
# forcely order that each one are placed horizontally
layout.label(text = "Extra Transform:")
# translation
layout.label(text = 'Translation')
row = layout.row()
row.prop(self, 'extra_translation', text = '')
# rotation
layout.label(text = 'Rotation')
row = layout.row()
row.prop(self, 'extra_rotation', text = '')
def general_get_extra_transform(self) -> mathutils.Matrix:
return mathutils.Matrix.LocRotScale(
mathutils.Vector(self.extra_translation),
mathutils.Euler(self.extra_rotation, 'XYZ'),
mathutils.Vector((1.0, 1.0, 1.0)) # no scale
)
class SharedRailSectionInputProperty(): class SharedRailSectionInputProperty():
""" """
This class is served for user to pick the transition type of rail. This class is served for user to pick the transition type of rail.
@ -112,7 +156,7 @@ class SharedScrewRailInputProperty():
rail_screw_steps: bpy.props.IntProperty( rail_screw_steps: bpy.props.IntProperty(
name = "Steps", name = "Steps",
description = "The segment count per iteration. More segment, more smooth but lower performance.", description = "The segment count per iteration. More segment, more smooth but lower performance.",
default = 16, default = 28,
min = 1, min = 1,
) # type: ignore ) # type: ignore
@ -147,7 +191,8 @@ class BBP_OT_add_rail_section(SharedRailSectionInputProperty, bpy.types.Operator
lambda bm: _create_rail_section( lambda bm: _create_rail_section(
bm, self.general_get_is_monorail(), bm, self.general_get_is_monorail(),
c_DefaultRailRadius, c_DefaultRailSpan c_DefaultRailRadius, c_DefaultRailSpan
) ),
mathutils.Matrix.Identity(4)
) )
return {'FINISHED'} return {'FINISHED'}
@ -163,7 +208,8 @@ class BBP_OT_add_transition_section(bpy.types.Operator):
def execute(self, context): def execute(self, context):
_rail_creator_wrapper( _rail_creator_wrapper(
lambda bm: _create_transition_section(bm, c_DefaultRailRadius, c_DefaultRailSpan) lambda bm: _create_transition_section(bm, c_DefaultRailRadius, c_DefaultRailSpan),
mathutils.Matrix.Identity(4)
) )
return {'FINISHED'} return {'FINISHED'}
@ -171,7 +217,7 @@ class BBP_OT_add_transition_section(bpy.types.Operator):
layout = self.layout layout = self.layout
layout.label(text = 'No Options Available') layout.label(text = 'No Options Available')
class BBP_OT_add_straight_rail(SharedRailSectionInputProperty, SharedRailCapInputProperty, SharedStraightRailInputProperty, bpy.types.Operator): class BBP_OT_add_straight_rail(SharedExtraTransform, SharedRailSectionInputProperty, SharedRailCapInputProperty, SharedStraightRailInputProperty, bpy.types.Operator):
"""Add Straight Rail""" """Add Straight Rail"""
bl_idname = "bbp.add_straight_rail" bl_idname = "bbp.add_straight_rail"
bl_label = "Straight Rail" bl_label = "Straight Rail"
@ -184,7 +230,8 @@ class BBP_OT_add_straight_rail(SharedRailSectionInputProperty, SharedRailCapInpu
self.general_get_is_monorail(), c_DefaultRailRadius, c_DefaultRailSpan, self.general_get_is_monorail(), c_DefaultRailRadius, c_DefaultRailSpan,
self.general_get_rail_length(), 0, self.general_get_rail_length(), 0,
self.general_get_rail_start_cap(), self.general_get_rail_end_cap() self.general_get_rail_start_cap(), self.general_get_rail_end_cap()
) ),
self.general_get_extra_transform()
) )
return {'FINISHED'} return {'FINISHED'}
@ -196,8 +243,10 @@ class BBP_OT_add_straight_rail(SharedRailSectionInputProperty, SharedRailCapInpu
layout.separator() layout.separator()
layout.label(text = 'Rail Cap') layout.label(text = 'Rail Cap')
self.draw_rail_cap_input(layout) self.draw_rail_cap_input(layout)
layout.separator()
self.draw_extra_transform_input(layout)
class BBP_OT_add_transition_rail(SharedRailCapInputProperty, SharedStraightRailInputProperty, bpy.types.Operator): class BBP_OT_add_transition_rail(SharedExtraTransform, SharedRailCapInputProperty, SharedStraightRailInputProperty, bpy.types.Operator):
"""Add Transition Rail""" """Add Transition Rail"""
bl_idname = "bbp.add_transition_rail" bl_idname = "bbp.add_transition_rail"
bl_label = "Transition Rail" bl_label = "Transition Rail"
@ -210,7 +259,8 @@ class BBP_OT_add_transition_rail(SharedRailCapInputProperty, SharedStraightRailI
c_DefaultRailRadius, c_DefaultRailSpan, c_DefaultRailRadius, c_DefaultRailSpan,
self.general_get_rail_length(), self.general_get_rail_length(),
self.general_get_rail_start_cap(), self.general_get_rail_end_cap() self.general_get_rail_start_cap(), self.general_get_rail_end_cap()
) ),
self.general_get_extra_transform()
) )
return {'FINISHED'} return {'FINISHED'}
@ -221,8 +271,10 @@ class BBP_OT_add_transition_rail(SharedRailCapInputProperty, SharedStraightRailI
layout.separator() layout.separator()
layout.label(text = 'Rail Cap') layout.label(text = 'Rail Cap')
self.draw_rail_cap_input(layout) self.draw_rail_cap_input(layout)
layout.separator()
self.draw_extra_transform_input(layout)
class BBP_OT_add_side_rail(SharedRailCapInputProperty, SharedStraightRailInputProperty, bpy.types.Operator): class BBP_OT_add_side_rail(SharedExtraTransform, SharedRailCapInputProperty, SharedStraightRailInputProperty, bpy.types.Operator):
"""Add Side Rail""" """Add Side Rail"""
bl_idname = "bbp.add_side_rail" bl_idname = "bbp.add_side_rail"
bl_label = "Side Rail" bl_label = "Side Rail"
@ -246,7 +298,8 @@ class BBP_OT_add_side_rail(SharedRailCapInputProperty, SharedStraightRailInputPr
self.general_get_rail_length(), self.general_get_rail_length(),
c_NormalSideRailAngle if self.side_rail_type == 'NORMAL' else c_StoneSideRailAngle, c_NormalSideRailAngle if self.side_rail_type == 'NORMAL' else c_StoneSideRailAngle,
self.general_get_rail_start_cap(), self.general_get_rail_end_cap() self.general_get_rail_start_cap(), self.general_get_rail_end_cap()
) ),
self.general_get_extra_transform()
) )
return {'FINISHED'} return {'FINISHED'}
@ -258,8 +311,10 @@ class BBP_OT_add_side_rail(SharedRailCapInputProperty, SharedStraightRailInputPr
layout.separator() layout.separator()
layout.label(text = 'Rail Cap') layout.label(text = 'Rail Cap')
self.draw_rail_cap_input(layout) self.draw_rail_cap_input(layout)
layout.separator()
self.draw_extra_transform_input(layout)
class BBP_OT_add_arc_rail(SharedRailSectionInputProperty, SharedRailCapInputProperty, SharedScrewRailInputProperty, bpy.types.Operator): class BBP_OT_add_arc_rail(SharedExtraTransform, SharedRailSectionInputProperty, SharedRailCapInputProperty, SharedScrewRailInputProperty, bpy.types.Operator):
"""Add Arc Rail""" """Add Arc Rail"""
bl_idname = "bbp.add_arc_rail" bl_idname = "bbp.add_arc_rail"
bl_label = "Arc Rail" bl_label = "Arc Rail"
@ -281,7 +336,8 @@ class BBP_OT_add_arc_rail(SharedRailSectionInputProperty, SharedRailCapInputProp
self.general_get_rail_start_cap(), self.general_get_rail_end_cap(), self.general_get_rail_start_cap(), self.general_get_rail_end_cap(),
math.degrees(self.rail_screw_angle), 0, 1, # blender passed value is in radians math.degrees(self.rail_screw_angle), 0, 1, # blender passed value is in radians
self.general_get_rail_screw_steps(), self.general_get_rail_screw_radius() self.general_get_rail_screw_steps(), self.general_get_rail_screw_radius()
) ),
self.general_get_extra_transform()
) )
return {'FINISHED'} return {'FINISHED'}
@ -294,8 +350,10 @@ class BBP_OT_add_arc_rail(SharedRailSectionInputProperty, SharedRailCapInputProp
layout.separator() layout.separator()
layout.label(text = 'Rail Cap') layout.label(text = 'Rail Cap')
self.draw_rail_cap_input(layout) self.draw_rail_cap_input(layout)
layout.separator()
self.draw_extra_transform_input(layout)
class BBP_OT_add_spiral_rail(SharedRailCapInputProperty, SharedScrewRailInputProperty, bpy.types.Operator): class BBP_OT_add_spiral_rail(SharedExtraTransform, SharedRailCapInputProperty, SharedScrewRailInputProperty, bpy.types.Operator):
"""Add Spiral Rail""" """Add Spiral Rail"""
bl_idname = "bbp.add_spiral_rail" bl_idname = "bbp.add_spiral_rail"
bl_label = "Spiral Rail" bl_label = "Spiral Rail"
@ -323,7 +381,8 @@ class BBP_OT_add_spiral_rail(SharedRailCapInputProperty, SharedScrewRailInputPro
self.general_get_rail_start_cap(), self.general_get_rail_end_cap(), self.general_get_rail_start_cap(), self.general_get_rail_end_cap(),
360, self.rail_screw_screw, self.rail_screw_iterations, 360, self.rail_screw_screw, self.rail_screw_iterations,
self.general_get_rail_screw_steps(), self.general_get_rail_screw_radius() self.general_get_rail_screw_steps(), self.general_get_rail_screw_radius()
) ),
self.general_get_extra_transform()
) )
return {'FINISHED'} return {'FINISHED'}
@ -336,8 +395,10 @@ class BBP_OT_add_spiral_rail(SharedRailCapInputProperty, SharedScrewRailInputPro
layout.separator() layout.separator()
layout.label(text = 'Rail Cap') layout.label(text = 'Rail Cap')
self.draw_rail_cap_input(layout) self.draw_rail_cap_input(layout)
layout.separator()
self.draw_extra_transform_input(layout)
class BBP_OT_add_side_spiral_rail(SharedRailSectionInputProperty, SharedRailCapInputProperty, SharedScrewRailInputProperty, bpy.types.Operator): class BBP_OT_add_side_spiral_rail(SharedExtraTransform, SharedRailSectionInputProperty, SharedRailCapInputProperty, SharedScrewRailInputProperty, bpy.types.Operator):
"""Add Side Spiral Rail""" """Add Side Spiral Rail"""
bl_idname = "bbp.add_side_spiral_rail" bl_idname = "bbp.add_side_spiral_rail"
bl_label = "Side Spiral Rail" bl_label = "Side Spiral Rail"
@ -360,7 +421,8 @@ class BBP_OT_add_side_spiral_rail(SharedRailSectionInputProperty, SharedRailCapI
self.general_get_rail_start_cap(), self.general_get_rail_end_cap(), self.general_get_rail_start_cap(), self.general_get_rail_end_cap(),
360, c_SideSpiralRailScrew, self.rail_screw_iterations, 360, c_SideSpiralRailScrew, self.rail_screw_iterations,
self.general_get_rail_screw_steps(), self.general_get_rail_screw_radius() self.general_get_rail_screw_steps(), self.general_get_rail_screw_radius()
) ),
self.general_get_extra_transform()
) )
return {'FINISHED'} return {'FINISHED'}
@ -372,6 +434,8 @@ class BBP_OT_add_side_spiral_rail(SharedRailSectionInputProperty, SharedRailCapI
layout.separator() layout.separator()
layout.label(text = 'Rail Cap') layout.label(text = 'Rail Cap')
self.draw_rail_cap_input(layout) self.draw_rail_cap_input(layout)
layout.separator()
self.draw_extra_transform_input(layout)
#endregion #endregion
@ -460,7 +524,7 @@ def _bmesh_cap(bm: bmesh.types.BMesh, edges: list[bmesh.types.BMEdge]) -> None:
#region Real Rail Creators #region Real Rail Creators
def _rail_creator_wrapper(fct_poly_cret: typing.Callable[[bmesh.types.BMesh], None]) -> bpy.types.Object: def _rail_creator_wrapper(fct_poly_cret: typing.Callable[[bmesh.types.BMesh], None], extra_transform: mathutils.Matrix) -> bpy.types.Object:
# create mesh first # create mesh first
bm: bmesh.types.BMesh = bmesh.new() bm: bmesh.types.BMesh = bmesh.new()
@ -473,8 +537,6 @@ def _rail_creator_wrapper(fct_poly_cret: typing.Callable[[bmesh.types.BMesh], No
bm.free() bm.free()
# setup smooth for mesh # setup smooth for mesh
mesh.use_auto_smooth = True
mesh.auto_smooth_angle = math.radians(50)
mesh.shade_smooth() mesh.shade_smooth()
# create object and assoc with it # create object and assoc with it
@ -492,6 +554,8 @@ def _rail_creator_wrapper(fct_poly_cret: typing.Callable[[bmesh.types.BMesh], No
# move to cursor # move to cursor
UTIL_functions.add_into_scene_and_move_to_cursor(obj) UTIL_functions.add_into_scene_and_move_to_cursor(obj)
# add extra transform
obj.matrix_world = obj.matrix_world @ extra_transform
# select created object # select created object
UTIL_functions.select_certain_objects((obj, )) UTIL_functions.select_certain_objects((obj, ))

View File

@ -1,9 +1,9 @@
import bpy import bpy
from bpy_extras.wm_utils.progress_report import ProgressReport from bpy_extras.wm_utils.progress_report import ProgressReport
import tempfile, os, typing, re import tempfile, os, typing
from . import PROP_preferences, UTIL_ioport_shared from . import PROP_preferences, UTIL_ioport_shared
from . import UTIL_virtools_types, UTIL_functions, UTIL_file_browser, UTIL_blender_mesh, UTIL_ballance_texture, UTIL_icons_manager from . import UTIL_virtools_types, UTIL_functions, UTIL_file_browser, UTIL_blender_mesh, UTIL_ballance_texture, UTIL_icons_manager, UTIL_naming_convension
from . import PROP_virtools_group, PROP_virtools_material, PROP_virtools_mesh, PROP_virtools_texture from . import PROP_virtools_group, PROP_virtools_material, PROP_virtools_mesh, PROP_virtools_texture, PROP_ballance_map_info
from .PyBMap import bmap_wrapper as bmap from .PyBMap import bmap_wrapper as bmap
# define global tex save opt blender enum prop helper # define global tex save opt blender enum prop helper
@ -20,19 +20,26 @@ class BBP_OT_export_virtools(bpy.types.Operator, UTIL_file_browser.ExportVirtool
description = "Decide how texture saved if texture is specified as Use Global as its Save Options.", description = "Decide how texture saved if texture is specified as Use Global as its Save Options.",
items = _g_EnumHelper_CK_TEXTURE_SAVEOPTIONS.generate_items(), items = _g_EnumHelper_CK_TEXTURE_SAVEOPTIONS.generate_items(),
default = _g_EnumHelper_CK_TEXTURE_SAVEOPTIONS.to_selection(UTIL_virtools_types.CK_TEXTURE_SAVEOPTIONS.CKTEXTURE_EXTERNAL) default = _g_EnumHelper_CK_TEXTURE_SAVEOPTIONS.to_selection(UTIL_virtools_types.CK_TEXTURE_SAVEOPTIONS.CKTEXTURE_EXTERNAL)
) ) # type: ignore
use_compress: bpy.props.BoolProperty( use_compress: bpy.props.BoolProperty(
name="Use Compress", name="Use Compress",
description = "Whether use ZLib to compress result when saving composition.",
default = True, default = True,
) ) # type: ignore
compress_level: bpy.props.IntProperty( compress_level: bpy.props.IntProperty(
name = "Compress Level", name = "Compress Level",
description = "The ZLib compress level used by Virtools Engine when saving composition.", description = "The ZLib compress level used by Virtools Engine when saving composition.",
min = 1, max = 9, min = 1, max = 9,
default = 5, default = 5,
) ) # type: ignore
successive_sector: bpy.props.BoolProperty(
name="Successive Sector",
description = "Whether order exporter to use document specified sector count to make sure sector is successive.",
default = True,
) # type: ignore
@classmethod @classmethod
def poll(self, context): def poll(self, context):
@ -59,6 +66,7 @@ class BBP_OT_export_virtools(bpy.types.Operator, UTIL_file_browser.ExportVirtool
_g_EnumHelper_CK_TEXTURE_SAVEOPTIONS.get_selection(self.texture_save_opt), _g_EnumHelper_CK_TEXTURE_SAVEOPTIONS.get_selection(self.texture_save_opt),
self.use_compress, self.use_compress,
self.compress_level, self.compress_level,
self.successive_sector,
objls objls
) )
@ -84,6 +92,14 @@ class BBP_OT_export_virtools(bpy.types.Operator, UTIL_file_browser.ExportVirtool
if self.use_compress: if self.use_compress:
box.prop(self, 'compress_level') box.prop(self, 'compress_level')
# show sector info to notice user
layout.separator()
layout.label(text = 'Ballance Params')
box = layout.box()
map_info: PROP_ballance_map_info.RawBallanceMapInfo = PROP_ballance_map_info.get_raw_ballance_map_info(bpy.context.scene)
box.prop(self, 'successive_sector')
box.label(text = f'Map Sectors: {map_info.mSectorCount}')
_TObj3dPair = tuple[bpy.types.Object, bmap.BM3dObject] _TObj3dPair = tuple[bpy.types.Object, bmap.BM3dObject]
_TMeshPair = tuple[bpy.types.Object, bpy.types.Mesh, bmap.BMMesh] _TMeshPair = tuple[bpy.types.Object, bpy.types.Mesh, bmap.BMMesh]
_TMaterialPair = tuple[bpy.types.Material, bmap.BMMaterial] _TMaterialPair = tuple[bpy.types.Material, bmap.BMMaterial]
@ -95,6 +111,7 @@ def _export_virtools(
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,
successive_sector_: bool,
export_objects: tuple[bpy.types.Object, ...] export_objects: tuple[bpy.types.Object, ...]
) -> None: ) -> None:
@ -114,7 +131,7 @@ def _export_virtools(
obj3d_crets: tuple[_TObj3dPair, ...] = _prepare_virtools_3dobjects( obj3d_crets: tuple[_TObj3dPair, ...] = _prepare_virtools_3dobjects(
writer, progress, export_objects) writer, progress, export_objects)
# export group and 3dobject by prepared 3dobject # export group and 3dobject by prepared 3dobject
_export_virtools_groups(writer, progress, obj3d_crets) _export_virtools_groups(writer, progress, successive_sector_, obj3d_crets)
mesh_crets: tuple[_TMeshPair, ...] = _export_virtools_3dobjects( mesh_crets: tuple[_TMeshPair, ...] = _export_virtools_3dobjects(
writer, progress, obj3d_crets) writer, progress, obj3d_crets)
# export mesh # export mesh
@ -163,6 +180,7 @@ def _prepare_virtools_3dobjects(
def _export_virtools_groups( def _export_virtools_groups(
writer: bmap.BMFileWriter, writer: bmap.BMFileWriter,
progress: ProgressReport, progress: ProgressReport,
successive_sector: bool,
obj3d_crets: tuple[_TObj3dPair, ...] obj3d_crets: tuple[_TObj3dPair, ...]
) -> None: ) -> None:
# create virtools group # create virtools group
@ -170,8 +188,24 @@ def _export_virtools_groups(
# start saving # start saving
progress.enter_substeps(len(obj3d_crets), "Saving Groups") progress.enter_substeps(len(obj3d_crets), "Saving Groups")
# create group exporting helper # create sector group first if user ordered
group_cret_guard: VirtoolsGroupCreationGuard = VirtoolsGroupCreationGuard(writer) # This step is designed for ensure that the created sector group is successive.
#
# Due to the design of Ballance, Ballance rely on checking the existance of Sector_XX to get how many sectors this map have.
# Thus if there are no component in a sector, it still need to create a empty Sector_XX group, otherwise the game will crash
# or be ended at a accident sector.
#
# So we create all needed sector group in here to make sure exported virtools file can be read by Ballancde correctly.
if successive_sector:
map_info: PROP_ballance_map_info.RawBallanceMapInfo
map_info = PROP_ballance_map_info.get_raw_ballance_map_info(bpy.context.scene)
for i in range(map_info.mSectorCount):
gp_name: str = UTIL_naming_convension.build_name_from_sector_index(i + 1)
vtgroup: bmap.BMGroup | None = group_cret_map.get(gp_name, None)
if vtgroup is None:
vtgroup = writer.create_group()
vtgroup.set_name(gp_name)
group_cret_map[gp_name] = vtgroup
for obj3d, vtobj3d in obj3d_crets: for obj3d, vtobj3d in obj3d_crets:
# open group visitor # open group visitor
@ -180,8 +214,8 @@ def _export_virtools_groups(
# get group or create new group from guard # get group or create new group from guard
vtgroup: bmap.BMGroup | None = group_cret_map.get(gp_name, None) vtgroup: bmap.BMGroup | None = group_cret_map.get(gp_name, None)
if vtgroup is None: if vtgroup is None:
# note: no need to set name, guard has set it. vtgroup = writer.create_group()
vtgroup = group_cret_guard.create_group(gp_name) vtgroup.set_name(gp_name)
group_cret_map[gp_name] = vtgroup group_cret_map[gp_name] = vtgroup
# group this object # group this object
@ -471,73 +505,6 @@ def _save_virtools_document(
progress.step() progress.step()
progress.leave_substeps() progress.leave_substeps()
class VirtoolsGroupCreationGuard():
"""
This class is designed for ensure that the created sector group is successive.
Due to the design of Ballance, Ballance rely on checking the existance of Sector_XX to get how many sectors this map have.
Thus if there are no component in a sector, it still need to create a empty Sector_XX group, otherwise the game will crash
or be ended at a accident sector.
This class hook the operation of Virtools group creation and check all Sector group creation.
Create essential group to make Sector_XX group successive.
Thus all group creation in this module should be passed by this class.
"""
cRegexGroupSector: typing.ClassVar[re.Pattern] = re.compile('^Sector_(0[1-8]|[1-9][0-9]{1,2}|9)$')
@staticmethod
def __get_group_index(group_name: str) -> int | None:
"""
Return the sector index of group name if it is. Otherwise None.
"""
regex_result = VirtoolsGroupCreationGuard.cRegexGroupSector.match(group_name)
if regex_result is not None:
return int(regex_result.group(1))
else:
return None
@staticmethod
def __get_group_name(group_index: int) -> str:
"""
Output Sector group name by given sector index
"""
if group_index == 9:
return 'Sector_9'
else:
return f'Sector_{group_index:0>2d}'
__mWriter: bmap.BMFileWriter
__mSectors: list[bmap.BMGroup]
def __init__(self, assoc_writer: bmap.BMFileWriter):
self.__mWriter = assoc_writer
self.__mSectors = []
def create_group(self, group_name: str) -> bmap.BMGroup:
"""
The hooked group creation function.
Please note the passed group name argument is just for name checking.
This function will set group name for you, not like BMFileWriter.create_group() operated.
"""
# check whether it is sector group
# note: the return sector index is 1 based, not 0
sector_idx: int | None = VirtoolsGroupCreationGuard.__get_group_index(group_name)
# if it is regular group, return normal creation
if sector_idx is None:
gp: bmap.BMGroup = self.__mWriter.create_group()
gp.set_name(group_name)
return gp
# get from sector cahce list
# enlarge sector cache list if it is not fulfilled given sector index
while sector_idx > len(self.__mSectors):
gp: bmap.BMGroup = self.__mWriter.create_group()
self.__mSectors.append(gp)
gp.set_name(self.__get_group_name(len(self.__mSectors)))
# return ordered sector from sector caches
return self.__mSectors[sector_idx - 1]
def register() -> None: def register() -> None:
bpy.utils.register_class(BBP_OT_export_virtools) bpy.utils.register_class(BBP_OT_export_virtools)

View File

@ -2,8 +2,8 @@ import bpy
from bpy_extras.wm_utils.progress_report import ProgressReport from bpy_extras.wm_utils.progress_report import ProgressReport
import tempfile, os, typing import tempfile, os, typing
from . import PROP_preferences, UTIL_ioport_shared from . import PROP_preferences, UTIL_ioport_shared
from . import UTIL_virtools_types, UTIL_functions, UTIL_file_browser, UTIL_blender_mesh, UTIL_ballance_texture from . import UTIL_virtools_types, UTIL_functions, UTIL_file_browser, UTIL_blender_mesh, UTIL_ballance_texture, UTIL_naming_convension
from . import PROP_virtools_group, PROP_virtools_material, PROP_virtools_mesh, PROP_virtools_texture from . import PROP_virtools_group, PROP_virtools_material, PROP_virtools_mesh, PROP_virtools_texture, PROP_ballance_map_info
from .PyBMap import bmap_wrapper as bmap from .PyBMap import bmap_wrapper as bmap
class BBP_OT_import_virtools(bpy.types.Operator, UTIL_file_browser.ImportVirtoolsFile, UTIL_ioport_shared.ImportParams, UTIL_ioport_shared.VirtoolsParams): class BBP_OT_import_virtools(bpy.types.Operator, UTIL_file_browser.ImportVirtoolsFile, UTIL_ioport_shared.ImportParams, UTIL_ioport_shared.VirtoolsParams):
@ -344,10 +344,12 @@ def _import_virtools_groups(
reader: bmap.BMFileReader, reader: bmap.BMFileReader,
progress: ProgressReport, progress: ProgressReport,
obj3d_cret_map: dict[bmap.BM3dObject, bpy.types.Object] obj3d_cret_map: dict[bmap.BM3dObject, bpy.types.Object]
) -> dict[bmap.BM3dObject, bpy.types.Object]: ) -> None:
# we need iterate all groups to construct a reversed map # we need iterate all groups to construct a reversed map
# to indicate which groups should this 3dobject be grouped into. # to indicate which groups should this 3dobject be grouped into.
reverse_map: dict[bmap.BM3dObject, set[str]] = {} reverse_map: dict[bmap.BM3dObject, set[str]] = {}
# sector counter to record the maximum sector we have processed.
sector_count: int = 1
# prepare progress # prepare progress
progress.enter_substeps(reader.get_material_count(), "Loading Groups") progress.enter_substeps(reader.get_material_count(), "Loading Groups")
@ -357,6 +359,12 @@ def _import_virtools_groups(
group_name: str | None = vtgroup.get_name() group_name: str | None = vtgroup.get_name()
if group_name is None: continue if group_name is None: continue
# try extracting sector info
potential_sector_count: int | None = UTIL_naming_convension.extract_sector_from_name(group_name)
if potential_sector_count is not None:
sector_count = max(sector_count, potential_sector_count)
# creating map
for item in vtgroup.get_objects(): for item in vtgroup.get_objects():
# get or create set # get or create set
objgroups: set[str] = reverse_map.get(item, None) objgroups: set[str] = reverse_map.get(item, None)
@ -370,6 +378,11 @@ def _import_virtools_groups(
# step # step
progress.step() progress.step()
# assign to ballance map info according to gotten sector count
map_info: PROP_ballance_map_info.RawBallanceMapInfo = PROP_ballance_map_info.get_raw_ballance_map_info(bpy.context.scene)
map_info.mSectorCount = max(map_info.mSectorCount, sector_count)
PROP_ballance_map_info.set_raw_ballance_map_info(bpy.context.scene, map_info)
# leave progress # leave progress
progress.leave_substeps() progress.leave_substeps()

View File

@ -1,4 +1,5 @@
import bpy import bpy
from . import UTIL_functions
from . import PROP_virtools_material, PROP_preferences from . import PROP_virtools_material, PROP_preferences
class BBP_OT_fix_all_material(bpy.types.Operator): class BBP_OT_fix_all_material(bpy.types.Operator):
@ -9,7 +10,9 @@ class BBP_OT_fix_all_material(bpy.types.Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder() # only enable this when plugin have a valid ballance texture folder
# and we are in object mode
return PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder() and UTIL_functions.is_in_object_mode()
def invoke(self, context, event): def invoke(self, context, event):
wm = context.window_manager wm = context.window_manager

View File

@ -43,25 +43,25 @@ class BBP_PG_legacy_align_history(bpy.types.PropertyGroup):
align_x: bpy.props.BoolProperty( align_x: bpy.props.BoolProperty(
name = "X Position", name = "X Position",
default = False, default = False,
) ) # type: ignore
align_y: bpy.props.BoolProperty( align_y: bpy.props.BoolProperty(
name = "Y Position", name = "Y Position",
default = False, default = False,
) ) # type: ignore
align_z: bpy.props.BoolProperty( align_z: bpy.props.BoolProperty(
name = "Z Position", name = "Z Position",
default = False, default = False,
) ) # type: ignore
current_align_mode: bpy.props.EnumProperty( current_align_mode: bpy.props.EnumProperty(
name = "Current Object (Active Object)", name = "Current Object (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),
) ) # type: ignore
target_align_mode: bpy.props.EnumProperty( target_align_mode: bpy.props.EnumProperty(
name = "Target Objects (Other Objects)", name = "Target Objects (Other Objects)",
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),
) ) # type: ignore
#endregion #endregion
@ -109,21 +109,21 @@ class BBP_OT_legacy_align(bpy.types.Operator):
options = {'HIDDEN', 'SKIP_SAVE'}, options = {'HIDDEN', 'SKIP_SAVE'},
default = True, # default True value to make it as a "light" button, not a grey one. default = True, # default True value to make it as a "light" button, not a grey one.
update = apply_flag_updated, update = apply_flag_updated,
) ) # type: ignore
recursive_hinder: bpy.props.BoolProperty( recursive_hinder: bpy.props.BoolProperty(
name = "Recursive Hinder", name = "Recursive Hinder",
description = "An internal flag to prevent the loop calling to apply_flags's updator.", description = "An internal flag to prevent the loop calling to apply_flags's updator.",
options = {'HIDDEN', 'SKIP_SAVE'}, options = {'HIDDEN', 'SKIP_SAVE'},
default = False, default = False,
) ) # type: ignore
align_history : bpy.props.CollectionProperty( align_history : bpy.props.CollectionProperty(
name = "Historys", name = "Historys",
description = "Align history.", description = "Align history.",
type = BBP_PG_legacy_align_history, type = BBP_PG_legacy_align_history,
) ) # type: ignore
@classmethod @classmethod
def poll(self, context): def poll(cls, context):
return _check_align_requirement() return _check_align_requirement()
def invoke(self, context, event): def invoke(self, context, event):
@ -180,10 +180,13 @@ 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 not UTIL_functions.is_in_object_mode():
return False
# check current obj # check current obj
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 # check target obj with filter of current obj
length = len(bpy.context.selected_objects) length = len(bpy.context.selected_objects)
if bpy.context.active_object in bpy.context.selected_objects: if bpy.context.active_object in bpy.context.selected_objects:

View File

@ -38,7 +38,11 @@ class BBP_OT_select_object_by_virtools_group(bpy.types.Operator, PROP_virtools_g
description = "Selection mode", description = "Selection mode",
items = _g_EnumHelper_SelectMode.generate_items(), items = _g_EnumHelper_SelectMode.generate_items(),
default = _g_EnumHelper_SelectMode.to_selection(SelectMode.Intersect) default = _g_EnumHelper_SelectMode.to_selection(SelectMode.Intersect)
) ) # type: ignore
@classmethod
def poll(cls, context):
return UTIL_functions.is_in_object_mode()
def invoke(self, context, event): def invoke(self, context, event):
wm = context.window_manager wm = context.window_manager
@ -119,7 +123,7 @@ class BBP_OT_add_objects_virtools_group(bpy.types.Operator, PROP_virtools_group.
bl_options = {'UNDO'} bl_options = {'UNDO'}
@classmethod @classmethod
def poll(self, context): def poll(cls, context):
return len(bpy.context.selected_objects) != 0 return len(bpy.context.selected_objects) != 0
def invoke(self, context, event): def invoke(self, context, event):
@ -131,6 +135,7 @@ class BBP_OT_add_objects_virtools_group(bpy.types.Operator, PROP_virtools_group.
for obj in bpy.context.selected_objects: for obj in bpy.context.selected_objects:
with PROP_virtools_group.VirtoolsGroupsHelper(obj) as gp: with PROP_virtools_group.VirtoolsGroupsHelper(obj) as gp:
gp.add_group(group_name) gp.add_group(group_name)
self.report({'INFO'}, "Grouping objects successfully.")
return {'FINISHED'} return {'FINISHED'}
def draw(self, context): def draw(self, context):
@ -143,7 +148,7 @@ class BBP_OT_rm_objects_virtools_group(bpy.types.Operator, PROP_virtools_group.S
bl_options = {'UNDO'} bl_options = {'UNDO'}
@classmethod @classmethod
def poll(self, context): def poll(cls, context):
return len(bpy.context.selected_objects) != 0 return len(bpy.context.selected_objects) != 0
def invoke(self, context, event): def invoke(self, context, event):
@ -155,6 +160,7 @@ class BBP_OT_rm_objects_virtools_group(bpy.types.Operator, PROP_virtools_group.S
for obj in bpy.context.selected_objects: for obj in bpy.context.selected_objects:
with PROP_virtools_group.VirtoolsGroupsHelper(obj) as gp: with PROP_virtools_group.VirtoolsGroupsHelper(obj) as gp:
gp.remove_group(group_name) gp.remove_group(group_name)
self.report({'INFO'}, "Ungrouping objects successfully.")
return {'FINISHED'} return {'FINISHED'}
def draw(self, context): def draw(self, context):
@ -167,7 +173,7 @@ class BBP_OT_clear_objects_virtools_group(bpy.types.Operator):
bl_options = {'UNDO'} bl_options = {'UNDO'}
@classmethod @classmethod
def poll(self, context): def poll(cls, context):
return len(bpy.context.selected_objects) != 0 return len(bpy.context.selected_objects) != 0
def invoke(self, context, event): def invoke(self, context, event):
@ -179,6 +185,7 @@ class BBP_OT_clear_objects_virtools_group(bpy.types.Operator):
for obj in bpy.context.selected_objects: for obj in bpy.context.selected_objects:
with PROP_virtools_group.VirtoolsGroupsHelper(obj) as gp: with PROP_virtools_group.VirtoolsGroupsHelper(obj) as gp:
gp.clear_groups() gp.clear_groups()
self.report({'INFO'}, "Clear objects groups successfully.")
return {'FINISHED'} return {'FINISHED'}
#endregion #endregion

View File

@ -0,0 +1,81 @@
import bpy
import typing
from . import UTIL_functions
class RawBallanceMapInfo():
cSectorCount: typing.ClassVar[int] = 1
mSectorCount: int
def __init__(self, **kwargs):
self.mSectorCount = kwargs.get("mSectorCount", RawBallanceMapInfo.cSectorCount)
def regulate(self):
self.mSectorCount = UTIL_functions.clamp_int(self.mSectorCount, 1, 999)
#region Prop Decl & Getter Setter
class BBP_PG_ballance_map_info(bpy.types.PropertyGroup):
sector_count: bpy.props.IntProperty(
name = "Sector",
description = "The sector count of this Ballance map which is used in exporting map and may be changed when importing map.",
default = 1,
max = 999, min = 1,
soft_max = 8, soft_min = 1,
step = 1
) # type: ignore
def get_ballance_map_info(scene: bpy.types.Scene) -> BBP_PG_ballance_map_info:
return scene.ballance_map_info
def get_raw_ballance_map_info(scene: bpy.types.Scene) -> RawBallanceMapInfo:
props: BBP_PG_ballance_map_info = get_ballance_map_info(scene)
rawdata: RawBallanceMapInfo = RawBallanceMapInfo()
rawdata.mSectorCount = props.sector_count
rawdata.regulate()
return rawdata
def set_raw_ballance_map_info(scene: bpy.types.Scene, rawdata: RawBallanceMapInfo) -> None:
props: BBP_PG_ballance_map_info = get_ballance_map_info(scene)
props.sector_count = rawdata.mSectorCount
#endregion
class BBP_PT_ballance_map_info(bpy.types.Panel):
"""Show Ballance Map Infos."""
bl_label = "Ballance Map"
bl_idname = "BBP_PT_ballance_map_info"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "scene"
@classmethod
def poll(cls, context):
return context.scene is not None
def draw(self, context):
layout: bpy.types.UILayout = self.layout
target: bpy.types.Scene = context.scene
props: BBP_PG_ballance_map_info = get_ballance_map_info(target)
# show map sector count numberbox
layout.prop(props, 'sector_count')
def register() -> None:
# register
bpy.utils.register_class(BBP_PG_ballance_map_info)
bpy.utils.register_class(BBP_PT_ballance_map_info)
# add into scene metadata
bpy.types.Scene.ballance_map_info = bpy.props.PointerProperty(type = BBP_PG_ballance_map_info)
def unregister() -> None:
# del from scene metadata
del bpy.types.Scene.ballance_map_info
# unregister
bpy.utils.unregister_class(BBP_PG_ballance_map_info)
bpy.utils.unregister_class(BBP_PT_ballance_map_info)

View File

@ -8,7 +8,7 @@ class BBP_PG_virtools_group(bpy.types.PropertyGroup):
group_name: bpy.props.StringProperty( group_name: bpy.props.StringProperty(
name = "Group Name", name = "Group Name",
default = "" default = ""
) ) # type: ignore
def get_virtools_groups(obj: bpy.types.Object) -> bpy.types.CollectionProperty: def get_virtools_groups(obj: bpy.types.Object) -> bpy.types.CollectionProperty:
return obj.virtools_groups return obj.virtools_groups
@ -405,14 +405,14 @@ def register() -> None:
bpy.utils.register_class(BBP_OT_clear_virtools_groups) bpy.utils.register_class(BBP_OT_clear_virtools_groups)
bpy.utils.register_class(BBP_PT_virtools_groups) bpy.utils.register_class(BBP_PT_virtools_groups)
# add into scene metadata # add into object metadata
bpy.types.Object.virtools_groups = bpy.props.CollectionProperty(type = BBP_PG_virtools_group) bpy.types.Object.virtools_groups = bpy.props.CollectionProperty(type = BBP_PG_virtools_group)
bpy.types.Object.active_virtools_groups = bpy.props.IntProperty() bpy.types.Object.active_virtools_groups = bpy.props.IntProperty()
def unregister() -> None: def unregister() -> None:
# del from scene metadata # del from object metadata
del bpy.types.Scene.active_virtools_groups del bpy.types.Object.active_virtools_groups
del bpy.types.Scene.virtools_groups del bpy.types.Object.virtools_groups
bpy.utils.unregister_class(BBP_PT_virtools_groups) bpy.utils.unregister_class(BBP_PT_virtools_groups)
bpy.utils.unregister_class(BBP_OT_clear_virtools_groups) bpy.utils.unregister_class(BBP_OT_clear_virtools_groups)

View File

@ -397,13 +397,20 @@ def apply_to_blender_material(mtl: bpy.types.Material):
# bnode.inputs["Emission"].default_value = rawdata.mEmissive.to_const_rgba() # bnode.inputs["Emission"].default_value = rawdata.mEmissive.to_const_rgba()
mtl.specular_intensity = rawdata.mSpecularPower mtl.specular_intensity = rawdata.mSpecularPower
bnode.inputs["Specular"].default_value = UTIL_functions.clamp_float( bnode.inputs["Specular IOR Level"].default_value = UTIL_functions.clamp_float(
rawdata.mSpecularPower, 0.0, 1.0 rawdata.mSpecularPower, 0.0, 1.0
) )
# set some alpha data # set some alpha data
mtl.use_backface_culling = not rawdata.mEnableTwoSided mtl.use_backface_culling = not rawdata.mEnableTwoSided
mtl.blend_method = 'BLEND' if rawdata.mEnableAlphaBlend else 'OPAQUE' if rawdata.mEnableAlphaBlend:
# In old format: mtl.blend_method = 'BLEND'
mtl.surface_render_method = 'BLENDED'
mtl.use_raytrace_refraction = True
else:
# In old format: mtl.blend_method = 'OPAQUE'
mtl.surface_render_method = 'DITHERED'
mtl.use_raytrace_refraction = False
# set texture # set texture
if rawdata.mTexture is not None: if rawdata.mTexture is not None:

View File

@ -107,8 +107,8 @@ class TemporaryMesh():
""" """
__mBindingObject: bpy.types.Object | None __mBindingObject: bpy.types.Object
__mTempMesh: bpy.types.Mesh | None __mTempMesh: bpy.types.Mesh
def __init__(self, binding_obj: bpy.types.Object): def __init__(self, binding_obj: bpy.types.Object):
self.__mBindingObject = binding_obj self.__mBindingObject = binding_obj
@ -149,7 +149,7 @@ class MeshReader():
A helper class TemporaryMesh can help you do this. A helper class TemporaryMesh can help you do this.
""" """
__mAssocMesh: bpy.types.Mesh | None ##< The binding mesh for this reader. None if this reader is invalid. __mAssocMesh: bpy.types.Mesh ##< The binding mesh for this reader. None if this reader is invalid.
def __init__(self, assoc_mesh: bpy.types.Mesh): def __init__(self, assoc_mesh: bpy.types.Mesh):
self.__mAssocMesh = assoc_mesh self.__mAssocMesh = assoc_mesh
@ -157,7 +157,6 @@ class MeshReader():
# triangulate temp mesh # triangulate temp mesh
if self.is_valid(): if self.is_valid():
self.__triangulate_mesh() self.__triangulate_mesh()
self.__mAssocMesh.calc_normals_split()
def is_valid(self) -> bool: def is_valid(self) -> bool:
return self.__mAssocMesh is not None return self.__mAssocMesh is not None
@ -171,7 +170,6 @@ class MeshReader():
def dispose(self) -> None: def dispose(self) -> None:
if self.is_valid(): if self.is_valid():
# reset mesh # reset mesh
self.__mAssocMesh.free_normals_split()
self.__mAssocMesh = None self.__mAssocMesh = None
def get_vertex_position_count(self) -> int: def get_vertex_position_count(self) -> int:
@ -203,10 +201,10 @@ class MeshReader():
raise UTIL_functions.BBPException('try to call an invalid MeshReader.') raise UTIL_functions.BBPException('try to call an invalid MeshReader.')
cache: UTIL_virtools_types.VxVector3 = UTIL_virtools_types.VxVector3() cache: UTIL_virtools_types.VxVector3 = UTIL_virtools_types.VxVector3()
for nml in self.__mAssocMesh.loops: for nml in self.__mAssocMesh.corner_normals:
cache.x = nml.normal.x cache.x = nml.vector.x
cache.y = nml.normal.y cache.y = nml.vector.y
cache.z = nml.normal.z cache.z = nml.vector.z
yield cache yield cache
def get_vertex_uv_count(self) -> int: def get_vertex_uv_count(self) -> int:
@ -318,7 +316,7 @@ class MeshWriter():
then refer it to all face uv. then refer it to all face uv.
""" """
__mAssocMesh: bpy.types.Mesh | None ##< The binding mesh for this writer. None if this writer is invalid. __mAssocMesh: bpy.types.Mesh ##< The binding mesh for this writer. None if this writer is invalid.
__mVertexPos: array.array ##< Array item is float(f). Length must be an integer multiple of 3. __mVertexPos: array.array ##< Array item is float(f). Length must be an integer multiple of 3.
__mVertexNormal: array.array ##< Array item is float(f). Length must be an integer multiple of 3. __mVertexNormal: array.array ##< Array item is float(f). Length must be an integer multiple of 3.
@ -348,6 +346,9 @@ class MeshWriter():
# Value is key's index in __mMtlSlot. # Value is key's index in __mMtlSlot.
__mMtlSlotMap: dict[bpy.types.Material | None, int] __mMtlSlotMap: dict[bpy.types.Material | None, int]
## The attribute name storing temporary normals data inside mesh.
__cTempNormalAttrName: typing.ClassVar[str] = 'temp_custom_normals'
def __init__(self, assoc_mesh: bpy.types.Mesh): def __init__(self, assoc_mesh: bpy.types.Mesh):
self.__mAssocMesh = assoc_mesh self.__mAssocMesh = assoc_mesh
@ -446,15 +447,19 @@ class MeshWriter():
self.__mAssocMesh.polygons.add(len(self.__mFaceVertexCount)) self.__mAssocMesh.polygons.add(len(self.__mFaceVertexCount))
# create uv layer # create uv layer
self.__mAssocMesh.uv_layers.new(do_init = False) self.__mAssocMesh.uv_layers.new(do_init = False)
# split normals, it is IMPORTANT
self.__mAssocMesh.create_normals_split()
# add vertex position data # add vertex position data
self.__mAssocMesh.vertices.foreach_set('co', self.__mVertexPos) self.__mAssocMesh.vertices.foreach_set('co', self.__mVertexPos)
# add face vertex pos index data # add face vertex pos index data
self.__mAssocMesh.loops.foreach_set('vertex_index', self.__mFacePosIndices) self.__mAssocMesh.loops.foreach_set('vertex_index', self.__mFacePosIndices)
# add face vertex nml by function # add face vertex nml by function via mesh custom attribute
self.__mAssocMesh.loops.foreach_set('normal', # NOTE: Blender 4.0 / 4.1 changed. I copy these code from FBX Importer.
temp_normal_attribute: bpy.types.FloatVectorAttribute
temp_normal_attribute = typing.cast(
bpy.types.FloatVectorAttribute,
self.__mAssocMesh.attributes.new(MeshWriter.__cTempNormalAttrName, 'FLOAT_VECTOR', 'CORNER')
)
temp_normal_attribute.data.foreach_set('vector',
tuple(_flat_face_nml_index(self.__mFaceNmlIndices, self.__mVertexNormal)) tuple(_flat_face_nml_index(self.__mFaceNmlIndices, self.__mVertexNormal))
) )
# add face vertex uv by function # add face vertex uv by function
@ -494,14 +499,22 @@ class MeshWriter():
# this should not happend in normal case, for testing, please load "Level_1.NMO" (Ballance Level 1). # this should not happend in normal case, for testing, please load "Level_1.NMO" (Ballance Level 1).
# copy data from loops preserved in validate(). # copy data from loops preserved in validate().
# NOTE: Blender 4.0 / 4.1 changed. I copy these code from FBX Importer.
loops_normals = array.array('f', [0.0] * (len(self.__mAssocMesh.loops) * 3)) loops_normals = array.array('f', [0.0] * (len(self.__mAssocMesh.loops) * 3))
self.__mAssocMesh.loops.foreach_get('normal', loops_normals) temp_normal_attribute = typing.cast(
bpy.types.FloatVectorAttribute,
self.__mAssocMesh.attributes[MeshWriter.__cTempNormalAttrName]
)
temp_normal_attribute.data.foreach_get("vector", loops_normals)
# apply data # apply data
self.__mAssocMesh.normals_split_custom_set( self.__mAssocMesh.normals_split_custom_set(
tuple(_nest_custom_split_normal(loops_normals)) tuple(_nest_custom_split_normal(loops_normals))
) )
# enable auto smooth. it is IMPORTANT self.__mAssocMesh.attributes.remove(
self.__mAssocMesh.use_auto_smooth = True # MARK: idk why I need fucking get this attribute again.
# But if I were not, this function must raise bullshit exception!
self.__mAssocMesh.attributes[MeshWriter.__cTempNormalAttrName]
)
def __clear_mesh(self): def __clear_mesh(self):
if not self.is_valid(): if not self.is_valid():

View File

@ -345,6 +345,12 @@ def create_bme_struct(
# 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
# because mirror matrix will reverse triangle indice order.
# If matrix is mirror matrix, we need reverse it again in following procession,
# including getting uv, calculating normal and providing face data.
mirror_matrix: bool = _is_mirror_matrix(transform)
# prepare mesh part data # prepare mesh part data
mesh_part: UTIL_blender_mesh.MeshWriterIngredient = UTIL_blender_mesh.MeshWriterIngredient() mesh_part: UTIL_blender_mesh.MeshWriterIngredient = UTIL_blender_mesh.MeshWriterIngredient()
def vpos_iterator() -> typing.Iterator[UTIL_virtools_types.VxVector3]: def vpos_iterator() -> typing.Iterator[UTIL_virtools_types.VxVector3]:
@ -370,7 +376,12 @@ def create_bme_struct(
if face_nml_data is None: if face_nml_data is None:
# nml is null, we need compute by ourselves # nml is null, we need compute by ourselves
# get first 3 entries in indices list as the compution ref # get first 3 entries in indices list as the compution ref
face_indices_data: list[int] = face_data[TOKEN_FACES_INDICES] # please note that we may need reverse it
face_indices_data: list[int]
if mirror_matrix:
face_indices_data = face_data[TOKEN_FACES_INDICES][::-1]
else:
face_indices_data = face_data[TOKEN_FACES_INDICES][:]
# compute it by getting vertices info from prebuild vertices data # compute it by getting vertices info from prebuild vertices data
# because the normals is computed from transformed vertices # because the normals is computed from transformed vertices
# so no need to correct its by normal transform. # so no need to correct its by normal transform.
@ -399,7 +410,9 @@ def create_bme_struct(
v: UTIL_virtools_types.VxVector2 = UTIL_virtools_types.VxVector2() v: UTIL_virtools_types.VxVector2 = UTIL_virtools_types.VxVector2()
for face_idx in valid_face_idx: for face_idx in valid_face_idx:
face_data: dict[str, typing.Any] = proto[TOKEN_FACES][face_idx] face_data: dict[str, typing.Any] = proto[TOKEN_FACES][face_idx]
for i in range(len(face_data[TOKEN_FACES_INDICES])): # iterate uv list considering mirror matrix
indices_count: int = len(face_data[TOKEN_FACES_INDICES])
for i in (range(indices_count)[::-1] if mirror_matrix else range(indices_count)):
# BME uv do not need any extra process # BME uv do not need any extra process
v.x, v.y = _eval_others(face_data[TOKEN_FACES_UVS][i], params) v.x, v.y = _eval_others(face_data[TOKEN_FACES_UVS][i], params)
yield v yield v
@ -424,8 +437,13 @@ def create_bme_struct(
# get face data # get face data
face_data: dict[str, typing.Any] = proto[TOKEN_FACES][face_idx] face_data: dict[str, typing.Any] = proto[TOKEN_FACES][face_idx]
# get face indices considering the mirror matrix
face_indices: list[int]
if mirror_matrix:
face_indices = face_data[TOKEN_FACES_INDICES][::-1]
else:
face_indices = face_data[TOKEN_FACES_INDICES][:]
# calc indices count # calc indices count
face_indices: list[int] = face_data[TOKEN_FACES_INDICES]
indices_count: int = len(face_indices) indices_count: int = len(face_indices)
# resize face data to fulfill req # resize face data to fulfill req
while len(f.mIndices) > indices_count: while len(f.mIndices) > indices_count:
@ -499,5 +517,18 @@ def _compute_normals(
corss_mul.normalize() corss_mul.normalize()
return (corss_mul.x, corss_mul.y, corss_mul.z) return (corss_mul.x, corss_mul.y, corss_mul.z)
def _is_mirror_matrix(mat: mathutils.Matrix) -> bool:
"""
Reflection matrix (aka. mirror matrix) is a special scaling matrix.
In this matrix, 1 or 3 scaling factor is minus number.
Mirror matrix will cause the inverse of triangle indice order.
So we need detect it and re-reverse when creating bm struct.
This function can detect whether given matrix is mirror matrix.
Reference: https://zhuanlan.zhihu.com/p/96717729
"""
return mat.is_negative
#return mat.to_3x3().determinant() < 0
#endregion #endregion

View File

@ -80,6 +80,16 @@ def select_certain_objects(objs: tuple[bpy.types.Object, ...]) -> None:
# select first object as active object # select first object as active object
bpy.context.view_layer.objects.active = objs[0] bpy.context.view_layer.objects.active = objs[0]
def is_in_object_mode() -> bool:
# get active object from context
obj = bpy.context.active_object
# if there is no active object, we think it is in object mode
if obj is None: return True
# simply check active object mode
return obj.mode == 'OBJECT'
class EnumPropHelper(): class EnumPropHelper():
""" """
These class contain all functions related to EnumProperty, including generating `items`, These class contain all functions related to EnumProperty, including generating `items`,

View File

@ -184,6 +184,42 @@ class BallanceObjectInfo():
#endregion #endregion
#region Sector Extractor
_g_RegexBlcSectorGroup: re.Pattern = re.compile('^Sector_(0[1-8]|[1-9][0-9]{1,2}|9)$')
def extract_sector_from_name(group_name: str) -> int | None:
"""
A convenient function to extract sector index from given group name.
This function also supports 999 sector plugin.
Not only in this module, but also in outside modules, this function is vary used to extract sector index info.
Function return the index extracted, or None if given group name is not a valid sector group.
The valid sector index is range from 1 to 999 (inclusive)
"""
regex_result = _g_RegexBlcSectorGroup.match(group_name)
if regex_result is not None:
return int(regex_result.group(1))
else:
return None
def build_name_from_sector_index(sector_index: int) -> str:
"""
A convenient function to build Ballance recognizable sector group name.
This function also supports 999 sector plugin.
This function also is used in this module or other modules outside.
Function return a sector name string. It basically the reverse operation of `extract_sector_from_name`.
"""
if sector_index == 9:
return 'Sector_9'
else:
return f'Sector_{sector_index:0>2d}'
#endregion
#region Naming Convention Declaration #region Naming Convention Declaration
_g_BlcNormalComponents: set[str] = set(( _g_BlcNormalComponents: set[str] = set((
@ -227,7 +263,6 @@ _g_BlcWood: set[str] = set((
)) ))
class VirtoolsGroupConvention(): class VirtoolsGroupConvention():
cRegexGroupSector: typing.ClassVar[re.Pattern] = re.compile('^Sector_(0[1-8]|[1-9][0-9]{1,2}|9)$')
cRegexComponent: typing.ClassVar[re.Pattern] = re.compile('^(' + '|'.join(_g_BlcNormalComponents) + ')_(0[1-9]|[1-9][0-9])_.*$') cRegexComponent: typing.ClassVar[re.Pattern] = re.compile('^(' + '|'.join(_g_BlcNormalComponents) + ')_(0[1-9]|[1-9][0-9])_.*$')
cRegexPC: typing.ClassVar[re.Pattern] = re.compile('^PC_TwoFlames_(0[1-7])$') cRegexPC: typing.ClassVar[re.Pattern] = re.compile('^PC_TwoFlames_(0[1-7])$')
cRegexPR: typing.ClassVar[re.Pattern] = re.compile('^PR_Resetpoint_(0[1-8])$') cRegexPR: typing.ClassVar[re.Pattern] = re.compile('^PR_Resetpoint_(0[1-8])$')
@ -255,9 +290,9 @@ class VirtoolsGroupConvention():
counter: int = 0 counter: int = 0
last_matched_sector: int = 0 last_matched_sector: int = 0
for i in gps: for i in gps:
regex_result = VirtoolsGroupConvention.cRegexGroupSector.match(i) regex_result: int | None = extract_sector_from_name(i)
if regex_result is not None: if regex_result is not None:
last_matched_sector = int(regex_result.group(1)) last_matched_sector = regex_result
counter += 1 counter += 1
if counter != 1: return None if counter != 1: return None
@ -364,12 +399,12 @@ class VirtoolsGroupConvention():
gp.add_group('Shadow') gp.add_group('Shadow')
case BallanceObjectType.RAIL: case BallanceObjectType.RAIL:
gp.add_group('Phys_FloorRails') gp.add_group('Phys_FloorRails')
gp.add_group('Sound_HitID_02')
gp.add_group('Sound_RollID_02')
case BallanceObjectType.WOOD:
gp.add_group('Phys_Floors')
gp.add_group('Sound_HitID_03') gp.add_group('Sound_HitID_03')
gp.add_group('Sound_RollID_03') gp.add_group('Sound_RollID_03')
case BallanceObjectType.WOOD:
gp.add_group('Phys_Floors')
gp.add_group('Sound_HitID_02')
gp.add_group('Sound_RollID_02')
case BallanceObjectType.STOPPER: case BallanceObjectType.STOPPER:
gp.add_group('Phys_FloorStopper') gp.add_group('Phys_FloorStopper')
@ -377,12 +412,8 @@ class VirtoolsGroupConvention():
# group into component type # group into component type
# use typing.cast() to force linter accept it because None is impossible # use typing.cast() to force linter accept it because None is impossible
gp.add_group(typing.cast(str, info.mComponentType)) gp.add_group(typing.cast(str, info.mComponentType))
# group to sector # group to sector
if info.mSector == 9: gp.add_group(build_name_from_sector_index(typing.cast(int, info.mSector)))
gp.add_group('Sector_9')
else:
gp.add_group(f'Sector_{info.mSector:0>2d}')
case _: case _:
if reporter is not None: if reporter is not None:
@ -460,9 +491,9 @@ class YYCToolchainConvention():
case BallanceObjectType.LEVEL_END: case BallanceObjectType.LEVEL_END:
return 'PE_Balloon_01' return 'PE_Balloon_01'
case BallanceObjectType.CHECKPOINT: case BallanceObjectType.CHECKPOINT:
return f'PR_TwoFlames_{info.mSector:0>2d}' return f'PC_TwoFlames_{info.mSector:0>2d}'
case BallanceObjectType.RESETPOINT: case BallanceObjectType.RESETPOINT:
return f'PC_Resetpoint_{info.mSector:0>2d}' return f'PR_Resetpoint_{info.mSector:0>2d}'
case BallanceObjectType.DEPTH_CUBE: case BallanceObjectType.DEPTH_CUBE:
return 'DepthCubes_' return 'DepthCubes_'

View File

@ -1,16 +1,3 @@
bl_info = {
"name": "Ballance Blender Plugin",
"description": "Ballance mapping tools for Blender",
"author": "yyc12345",
"version": (4, 0),
"blender": (3, 6, 0),
"category": "Object",
"support": "COMMUNITY",
"warning": "Please read document before using this plugin.",
"doc_url": "https://github.com/yyc12345/BallanceBlenderHelper",
"tracker_url": "https://github.com/yyc12345/BallanceBlenderHelper/issues"
}
#region Reload and Import #region Reload and Import
# import core lib # import core lib
@ -29,7 +16,8 @@ from . import UTIL_icons_manager
UTIL_icons_manager.register() UTIL_icons_manager.register()
# then load other modules # then load other modules
from . import PROP_preferences, PROP_ptrprop_resolver, PROP_virtools_material, PROP_virtools_texture, PROP_virtools_mesh, PROP_virtools_group, PROP_ballance_element, PROP_bme_material from . import PROP_preferences, PROP_ptrprop_resolver, PROP_virtools_material, PROP_virtools_texture, PROP_virtools_mesh, PROP_virtools_group
from . import PROP_ballance_element, PROP_bme_material, PROP_ballance_map_info
from . import OP_IMPORT_bmfile, OP_EXPORT_bmfile, OP_IMPORT_virtools, OP_EXPORT_virtools from . import OP_IMPORT_bmfile, OP_EXPORT_bmfile, OP_IMPORT_virtools, OP_EXPORT_virtools
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_material from . import OP_MTL_fix_material
@ -217,6 +205,7 @@ def register() -> None:
PROP_virtools_group.register() PROP_virtools_group.register()
PROP_ballance_element.register() PROP_ballance_element.register()
PROP_bme_material.register() PROP_bme_material.register()
PROP_ballance_map_info.register()
OP_IMPORT_bmfile.register() OP_IMPORT_bmfile.register()
OP_EXPORT_bmfile.register() OP_EXPORT_bmfile.register()
@ -275,6 +264,7 @@ def unregister() -> None:
OP_EXPORT_bmfile.unregister() OP_EXPORT_bmfile.unregister()
OP_IMPORT_bmfile.unregister() OP_IMPORT_bmfile.unregister()
PROP_ballance_map_info.unregister()
PROP_bme_material.unregister() PROP_bme_material.unregister()
PROP_ballance_element.unregister() PROP_ballance_element.unregister()
PROP_virtools_group.unregister() PROP_virtools_group.unregister()

View File

@ -0,0 +1,81 @@
# Full context are copied from https://docs.blender.org/manual/en/dev/extensions/getting_started.html
# Please note any update of this manifest
schema_version = "1.0.0"
# Example of manifest file for a Blender extension
# Change the values according to your extension
id = "bbp_ng"
version = "4.0.0"
name = "Ballance Blender Plugin"
tagline = "The specialized add-on served for creating game map of Ballance"
maintainer = "yyc12345 <yyc12321@outlook.com>"
# Supported types: "add-on", "theme"
type = "add-on"
# Optional link to documentation, support, source files, etc
website = "https://github.com/yyc12345/BallanceBlenderHelper"
# Optional list defined by Blender and server, see:
# https://docs.blender.org/manual/en/dev/advanced/extensions/tags.html
tags = ["Object", "Mesh", "UV", "Import-Export"]
blender_version_min = "4.2.0"
# # Optional: Blender version that the extension does not support, earlier versions are supported.
# # This can be omitted and defined later on the extensions platform if an issue is found.
# blender_version_max = "5.1.0"
# License conforming to https://spdx.org/licenses/ (use "SPDX: prefix)
# https://docs.blender.org/manual/en/dev/advanced/extensions/licenses.html
license = [
"SPDX:GPL-3.0-or-later",
]
# Optional: required by some licenses.
# copyright = [
# "2002-2024 Developer Name",
# "1998 Company Name",
# ]
# Optional list of supported platforms. If omitted, the extension will be available in all operating systems.
platforms = ["windows-x64", "linux-x64"]
# Supported platforms: "windows-x64", "macos-arm64", "linux-x64", "windows-arm64", "macos-x64"
# Optional: bundle 3rd party Python modules.
# https://docs.blender.org/manual/en/dev/advanced/extensions/python_wheels.html
# wheels = [
# "./wheels/hexdump-3.3-py3-none-any.whl",
# "./wheels/jsmin-3.0.1-py3-none-any.whl",
# ]
# Optional: add-ons can list which resources they will require:
# * files (for access of any filesystem operations)
# * network (for internet access)
# * clipboard (to read and/or write the system clipboard)
# * camera (to capture photos and videos)
# * microphone (to capture audio)
#
# If using network, remember to also check `bpy.app.online_access`
# https://docs.blender.org/manual/en/dev/advanced/extensions/addons.html#internet-access
#
# For each permission it is important to also specify the reason why it is required.
# Keep this a single short sentence without a period (.) at the end.
# For longer explanations use the documentation or detail page.
[permissions]
# network = "Need to sync motion-capture data to server"
files = "Import/export Virtools file from/to disk"
# clipboard = "Copy and paste bone transforms"
# Optional: build settings.
# https://docs.blender.org/manual/en/dev/advanced/extensions/command_line_arguments.html#command-line-args-extension-build
[build]
paths_exclude_pattern = [
"__pycache__/", # Python runtime cache
".style.yapf", # Python code style
"*.gitkeep", # Git directory keeper
".gitignore", # Git Ignore File
"*.md", # Useless document.
"/raw_jsons", # Raw JSONs.
"/raw_icons", # Raw Icons.
"/tools", # Assistant tools.
]

View File

@ -43,11 +43,11 @@
"params": { "params": {
"length": "length", "length": "length",
"height": "height", "height": "height",
"face": "(face[0], face[1], face[3], face[2], face[5], False)", "face": "(face[0], face[1], face[2], face[3], face[5], False)",
"is_sink": "is_sink", "is_sink": "is_sink",
"is_ribbon": "False" "is_ribbon": "False"
}, },
"transform": "move(length, 5, 0) @ rot(0, 0, 180)" "transform": "move(0, 5, 0) @ scale(1, -1, 1)"
} }
] ]
}, },

View File

@ -15,7 +15,7 @@ def resize_image(src_file: str, dst_file: str) -> None:
def create_thumbnails() -> None: def create_thumbnails() -> None:
# get folder path # get folder path
root_folder: str = os.path.dirname(os.path.dirname(__file__)) root_folder: str = common.get_plugin_folder()
# prepare handler # prepare handler
def folder_handler(src_folder: str, dst_folder: str) -> None: def folder_handler(src_folder: str, dst_folder: str) -> None:

View File

@ -14,7 +14,7 @@ def compress_json(src_file: str, dst_file: str) -> None:
def create_compressed_jsons() -> None: def create_compressed_jsons() -> None:
# get folder path # get folder path
root_folder: str = os.path.dirname(os.path.dirname(__file__)) root_folder: str = common.get_plugin_folder()
# prepare handler # prepare handler
def folder_handler(src_folder: str, dst_folder: str) -> None: def folder_handler(src_folder: str, dst_folder: str) -> None:

View File

@ -1,4 +1,12 @@
import os, typing import os, typing, fnmatch, shutil
def get_plugin_folder() -> str:
"""
Get the absolute path to plugin root folder.
@return The absolute path to plugin root folder.
"""
return os.path.dirname(os.path.dirname(__file__))
def relative_to_folder(abs_path: str, src_parent: str, dst_parent: str) -> str: def relative_to_folder(abs_path: str, src_parent: str, dst_parent: str) -> str:
""" """

View File

@ -0,0 +1,4 @@
# Ballance Properties
!!! info "Work in Progress"
This part of manual still work in progress.

View File

@ -19,6 +19,7 @@ Therefore, choosing Blender with BBP for mapping is not only choosing freedom an
## Features ## Features
* [Virtools Properties](./virtools-properties.md) * [Virtools Properties](./virtools-properties.md)
* [Ballance Properties](./ballance-properties.md)
* [Import and Export Virtools Document](./import-export-virtools.md) * [Import and Export Virtools Document](./import-export-virtools.md)
* [Group Operation](./group-operations.md) * [Group Operation](./group-operations.md)
* [Legacy Alignment](./legacy-align.md) * [Legacy Alignment](./legacy-align.md)

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

@ -0,0 +1,29 @@
# Ballance属性
Ballance属性有别于Virtools属性它是专门为Ballance制图服务的一系列属性。这些属性寄宿于场景在同一场景中制图不会涉及Blender中的场景切换这些属性不会改变。在`Scene`属性面板可以找到Ballance属性相关的面板如下图所示分别是
* `Ballance Elements`面板红色箭头对应Ballance机关
* `BME Materials`面板绿色箭头对应BME材质
* `Ballance Map`面板蓝色箭头对应Ballance地图信息
其中只有Ballance地图信息是你需要重点关注的其它属性在通常情况下不需要关注除非地图中的某些材质或网格出现错误后才需要关注这些属性。
![](../imgs/ballance-properties.png)
## Ballance地图信息
Ballance地图信息目前只有一个选项Sector地图小节数。此属性指示了当前地图的最终期望小节数。这个属性主要是用于解决导出地图Bug的具体Bug内容可参考导入导出Virtools文档章节。
你需要做的唯一一件事就是在导出最终地图前检查此字段是否是你期望的小节数。需要注意的是尽管你可以在创建地图的一开始就设置此字段然而BBP中的其它一些功能可能会修改此字段比如添加机关导入Virtools文档等。例如当你的地图小节指定为3你正在添加一个归属于第4小节的机关那么此值会自动增加到4。同理在导入一个总共4小节的Ballance地图时此值也会增加到4如果先前值小于4的话。这主要是为了用户可以在没有感知到此值的情况下使用此插件尤其是在对某些现有地图进行略微修改时。然而如此操作可能会在某些情况下不能满足用户的需求所以仍建议你在导出前检查此字段。
## Ballance机关
Ballance机关记录了你所有使用BBP添加机关功能添加的机关的网格。在3D文件中通常而言网格占据了最大的数据量因此减少网格的数量即通过在各个相同形状的物体之间共享网格可以大幅减少地图文件所占用的大小。当你使用BBP添加机关相关功能时会首先尝试从这里获取机关的网格如果没有对应网格则加载并记录在这里。
这个面板只供查看不可编辑。当你不小心修改了BBP添加的机关的网格它们本不该被修改想要恢复其原有形状时只需点击`Reset Ballance Elements`即可将列表中的所有机关网格重置为正确状态。
## BME材质
与Ballance机关类似记录了使用BME添加路面时使用的材质。这也是为了可以复用材质而设计的这样就不需要每创建一个物体就创建一套与之相关的材质大大减少了重复材质的数量。
这个面板只供查看不可编辑。当你不小心修改了BME相关的材质它们本不该被修改想要恢复其原有材质时只需点击`Reset BME Materials`即可将列表中的所有材质重置为正确状态。

View File

@ -20,7 +20,11 @@ BBP内置了一系列自定义图标以及其组件BME需要的用于描述
## 打包 ## 打包
`bbp_ng`文件夹压缩成ZIP文件即可完成打包工作。需要注意的是下列文件或文件夹不应被打包 从Blender 4.2 LTS开始插件使用Blender自带的打包功能进行打包
假定在项目根目录下执行命令,最终输出文件为`redist/bbp_ng.zip`,那么在命令行窗口中执行`blender --command extension build --source-dir bbp_ng --output-filepath redist/bbp_ng.zip`命令即可完成打包。其中`blender`为Blender的可执行程序。
Blender会根据`blender_manifest.toml`的指示,在排除下列文件的情况下将插件打包:
* `bbp_ng/raw_icons`:原始图片文件夹。 * `bbp_ng/raw_icons`:原始图片文件夹。
* `bbp_ng/raw_jsons`原始JSON文件夹。 * `bbp_ng/raw_jsons`原始JSON文件夹。
@ -30,10 +34,6 @@ BBP内置了一系列自定义图标以及其组件BME需要的用于描述
* `bbp_ng/icons/.gitkeep`:文件夹占位符 * `bbp_ng/icons/.gitkeep`:文件夹占位符
* `bbp_ng/jsons/.gitkeep`:文件夹占位符 * `bbp_ng/jsons/.gitkeep`:文件夹占位符
打包后的ZIP文件打开后如果有且只有`bbp_ng`一个文件夹,则代表打包成功。切勿直接将`bbp_ng` **内部的文件** 直接打包到ZIP文件中。
这样打包后的ZIP文件既可以直接通过Blender插件的安装功能直接安装也可以解压在插件目录下完成安装。
## 生成帮助文档 ## 生成帮助文档
虽然本项目会利用GitHub Page功能提供帮助文档但有时你可能需要提供帮助文档的离线版本本节将会介绍如何生成离线版本的帮助文档。 虽然本项目会利用GitHub Page功能提供帮助文档但有时你可能需要提供帮助文档的离线版本本节将会介绍如何生成离线版本的帮助文档。

View File

@ -7,7 +7,7 @@
## 打开配置面板 ## 打开配置面板
开启Blender选择`Edit - Preferences`,在打开的窗口中转到`Add-ons`选项卡,在`Community`分类下找到BBP插件名称为`Object: Ballance Blender Plugin`。请确保其左侧的勾已被选中,代表插件已被启用。点击勾左侧的三角箭头展开插件详细信息,如图所示,进入配置面板。 开启Blender选择`Edit - Preferences`,在打开的窗口中转到`Add-ons`选项卡,在列表中找到BBP插件名称为`Ballance Blender Plugin`。请确保其左侧的勾已被选中,代表插件已被启用。点击勾左侧的三角箭头展开插件详细信息,如图所示,进入配置面板。
![](../imgs/config-plugin.png) ![](../imgs/config-plugin.png)

View File

@ -46,3 +46,10 @@ Global Texture Save Option全局贴图保存选项决定了那些设置了
Use Compress使用压缩属性指定保存的文档是否压缩存储。压缩可以显著减少文档体积且在现代计算机平台上压缩所造成的性能损失几乎可以忽略不计。当选择使用压缩后一个额外的Compress Level压缩等级属性将会显示用于指定压缩的级别数值越高压缩率越大文件越小。 Use Compress使用压缩属性指定保存的文档是否压缩存储。压缩可以显著减少文档体积且在现代计算机平台上压缩所造成的性能损失几乎可以忽略不计。当选择使用压缩后一个额外的Compress Level压缩等级属性将会显示用于指定压缩的级别数值越高压缩率越大文件越小。
### Ballance参数
Ballance ParamsBallance参数章节包含针对Ballance特有内容对导出过程进行优化的参数。
Successive Sector小节连续是一个解决导出小节组时出现的Bug的选项。由于某些原因如果一个小节中没有任何机关实际上是某小节组中没有归入任何物体导出插件会认为该小节组不存在因而遗漏导出。且由于Ballance对最终小节即飞船出现小节的判定是从1开始递增寻找最后一个存在的小节组所以二者叠加会导致Ballance错误地认定地图的小节数从而在错误的小节显示飞船这也就是导出Bug。当勾选此选项后导出文档时会预先按照当前Blender文件中Ballance地图信息中指定的小节数预先创建所有小节组然后再进行导出这样就不会遗漏创建某些小节组飞船也会在正确的小节显示。
这个选项通常在导出可游玩的地图时选中,如果你只是想导出一些模型,那么需要关闭此选项,否则会在最终文件中产生许多无用的小节组。

View File

@ -16,6 +16,7 @@ Ballance Blender Plugin后文简称BBP是一款关注Ballance自制地图
## 特性 ## 特性
* [Virtools属性](./virtools-properties.md) * [Virtools属性](./virtools-properties.md)
* [Ballance属性](./ballance-properties.md)
* [导入导出Virtools文档](./import-export-virtools.md) * [导入导出Virtools文档](./import-export-virtools.md)
* [按组操作](./group-operations.md) * [按组操作](./group-operations.md)
* [传统对齐](./legacy-align.md) * [传统对齐](./legacy-align.md)

View File

@ -2,19 +2,26 @@
## 明确版本 ## 明确版本
BBP对Blender支持的原则是支持当前最新的 **LTS** 版本在最新的LTS版本释出之后会花一些时间迁移插件。当前插件版本 **4.0**基于Blender **3.6.x** 版本。 BBP对Blender支持的原则是支持当前最新的 **LTS** 版本在最新的LTS版本释出之后会花一些时间迁移插件。当前插件版本 **4.0**基于Blender **4.2.x** 版本。
理论上而言如果Blender没有做出重大改动那么BBP可以在其它版本上正常运行。例如你可以尝试在Blender 4.0上运行基于Blender 3.6的BBP插件。但BBP的开发者不会处理仅在非LTS版本中才出现的Bug。在安装插件之前请先选择适合的版本。 理论上而言如果Blender没有做出重大改动那么BBP可以在其它版本上正常运行。例如你可以尝试在Blender 4.0上运行基于Blender 3.6 LTS的BBP插件。但BBP的开发者不会处理仅在非LTS版本中才出现的Bug。在安装插件之前请先选择适合的版本。
## 卸载旧插件 ## 卸载旧插件
如果你之前使用过BBP那么你需要首先卸载它。旧版的BBP通常被安装在下列的位置中 如果你之前使用过BBP那么你需要首先卸载它。旧版的BBP通常被安装在下列的位置中
* `Blender/3.6/scripts/addons/ballance_blender_plugin` * `<Blender>/3.6/scripts/addons/ballance_blender_plugin`BBP 3.0或更低版本
* `Blender/3.6/scripts/addons_contrib/ballance_blender_plugin` * `<Blender>/3.6/scripts/addons_contrib/ballance_blender_plugin`BBP 3.0或更低版本
* `Blender/3.6/scripts/addons/bbp_ng` * `<Blender>/3.6/scripts/addons/bbp_ng`BBP 4.0内测版本
* `%APPDATA%/Blender Foundation/Blender/3.6/scripts/addons/bbp_ng`BBP 4.0内测版本
* `%APPDATA%/Blender Foundation/Blender/4.2/extensions/user_default/bbp_ng`BBP 4.0或更高版本
你只需要删除这些文件夹(如果它们存在的话)即可完全卸载插件。路径中的`Blender`指代你的Blender安装位置。路径中的`3.6`是你安装的Blender的版本号需要根据你安装的版本进行调整本手册均以`3.6`为例 你只需要先在Blender中关闭插件把插件名前面的勾取消然后再删除这些文件夹(如果它们存在的话)即可完全卸载插件。路径中的`<Blender>`指代你的Blender安装位置。路径中的`3.6``4.2`是你安装的Blender的版本号需要根据你安装的版本进行调整后续出现的版本号也按此理解
!!! warning "不能使用Blender的插件卸载功能"
不能使用Blender插件页面的插件卸载功能卸载BBP因为BBP只要被Blender加载无论是否启用都会将Virtools文件读写库BMap加载进Blender。若在Blender运行期间删除会出现拒绝访问错误。因此您必须在关闭Blender后手动删除插件目录。
如果您实在无法确定插件安装到了哪里可以在Blender的偏好设置中的插件页面里找到`File`属性,其指向文件所在的文件夹就是要删除的文件夹。
!!! info "`ballance_blender_plugin``bbp_ng`" !!! info "`ballance_blender_plugin``bbp_ng`"
`ballance_blender_plugin`是旧版BBP插件4.0版本前)的模块名,`bbp_ng`是新版BBP插件4.0版本后包括4.0版本)的模块名。为了保证用户确实删除了旧版插件,所以同时提供了这两者。 `ballance_blender_plugin`是旧版BBP插件4.0版本前)的模块名,`bbp_ng`是新版BBP插件4.0版本后包括4.0版本)的模块名。为了保证用户确实删除了旧版插件,所以同时提供了这两者。
@ -22,6 +29,9 @@ BBP对Blender支持的原则是支持当前最新的 **LTS** 版本,在最新
!!! info "`addons``addons_contrib`" !!! info "`addons``addons_contrib`"
在Blender 3.6 LTS版本即BBP 3.3版本之后Blender不再支持Testing类型插件。因而导致安装Testing插件专用的`addons_contrib`文件夹不再使用,插件需要被统一安装在`addons`中。为了保证用户确实删除了旧版插件,所以同时提供了这两者。 在Blender 3.6 LTS版本即BBP 3.3版本之后Blender不再支持Testing类型插件。因而导致安装Testing插件专用的`addons_contrib`文件夹不再使用,插件需要被统一安装在`addons`中。为了保证用户确实删除了旧版插件,所以同时提供了这两者。
!!! info "`addons``extensions`"
在Blender 4.2 LTS版本Blender使用扩展Extensions而非插件Addons来描述插件。因而导致安装插件的位置也发生了变化。为了保证用户确实删除了旧版插件所以同时提供了这两者。
## 下载插件 ## 下载插件
你可以通过[本工程的GitHub代码库的Release页面](https://github.com/yyc12345/BallanceBlenderHelper/releases)下载最新的插件。插件是以ZIP压缩包形式提供的。 你可以通过[本工程的GitHub代码库的Release页面](https://github.com/yyc12345/BallanceBlenderHelper/releases)下载最新的插件。插件是以ZIP压缩包形式提供的。
@ -36,11 +46,11 @@ BBP对Blender支持的原则是支持当前最新的 **LTS** 版本,在最新
## 安装插件 ## 安装插件
开启Blender选择`Edit - Preferences`,在打开的窗口中转到`Add-ons`选项卡,点击`Install...`按钮选择刚刚下载完毕的ZIP压缩包即可安装完成。若没有在列表中看到可选择刷新按钮或重启Blender。 开启Blender选择`Edit - Preferences`,在打开的窗口中转到`Add-ons`选项卡,点击窗口右上方的箭头,然后点击`Install from Disk...`按钮选择刚刚下载完毕的ZIP压缩包即可安装完成。若没有在列表中看到可选择刷新按钮或重启Blender。
你也可以选择手动安装插件(如果上述安装方法失败了的话),转到`Blender/3.6/scripts/addons`将下载好的ZIP压缩包内容解压到此文件夹下启动Blender即可在插件列表中找到BBP。 你也可以选择手动安装插件(如果上述安装方法失败了的话),转到`%APPDATA%/Blender Foundation/Blender/4.2/extensions/user_default`,创建一个名为`bbp_ng`的文件夹并进入将下载好的ZIP压缩包内容解压到此文件夹下启动Blender即可在插件列表中找到BBP。
BBP插件位于`Community`类别下,名称为`Object: Ballance Blender Plugin`,找到后勾选名称左侧的勾即可启用插件。插件安装成功后的偏好设置页面如下图所示。 BBP插件在列表中的名称为`Ballance Blender Plugin`,找到后勾选名称左侧的勾即可启用插件。插件安装成功后的偏好设置页面如下图所示。
![](../imgs/config-plugin.png) ![](../imgs/config-plugin.png)

View File

@ -8,7 +8,7 @@
## 使用方法 ## 使用方法
传统对齐支持将多个物体对一个物体的对齐,操作方法是先依次选中需要被对齐的物体,然后在最后选中对齐参考对象(也就是使其为活动物体),然后点击`Ballance - 3ds Max Align`,即可弹出传统对齐面板,之后便可开始对齐操作。 传统对齐支持将多个物体对一个物体的对齐,操作方法是先依次选中需要被对齐的物体,然后在最后选中对齐参考对象(也就是使其为活动物体),然后点击`Ballance - 3ds Max Align`,即可弹出传统对齐面板,之后便可开始对齐操作。
## 面板介绍 ## 面板介绍

View File

@ -4,4 +4,9 @@
* 制图工具链标准及`meshes`文件夹下的文件的格式https://github.com/yyc12345/gist/blob/master/BMFileSpec/YYCToolsChainSpec_ZH.md * 制图工具链标准及`meshes`文件夹下的文件的格式https://github.com/yyc12345/gist/blob/master/BMFileSpec/YYCToolsChainSpec_ZH.md
* BMERevenge的JSON文件的格式https://github.com/yyc12345/gist/blob/master/BMERevenge/DevDocument_v2.0_ZH.md * BMERevenge的JSON文件的格式https://github.com/yyc12345/gist/blob/master/BMERevenge/DevDocument_v2.0_ZH.md
本插件配合了`fake-bpy-module`模块来实现类型提示以加快开发速度。本插件目前基于Blender 3.6,因此使用`pip install fake-bpy-module-latest==20230627`来安装Blender的类型提示库。 这主要是因为`fake-bpy-module`没有发布官方的适用于Blender 3.6的包因此我只能通过选择最接近Blender 3.6版本发布时间的每日编译版本来安装它 本插件配合了`fake-bpy-module`模块来实现类型提示以加快开发速度。使用如下命令来安装Blender的类型提示库
* Blender 3.6: `pip install fake-bpy-module-latest==20230627`
* Blender 4.2: `pip install fake-bpy-module-latest==20240716`
这么做主要是因为`fake-bpy-module`没有发布官方的适用于指定Blender版本的包因此我只能通过选择最接近Blender对应版本发布时间的每日编译版本来安装它。

View File

@ -52,7 +52,7 @@ BBP插件为所有Blender贴图实际上是Image添加了新的属性
## Virtools网格 ## Virtools网格
BBP插件为所有Blender贴图实际上是Image添加了新的属性称为Virtools Mesh。转到`Data`属性面板,即可以找到`Virtools Mesh`面板。 BBP插件为所有Blender网格添加了新的属性称为Virtools Mesh。转到`Data`属性面板,即可以找到`Virtools Mesh`面板。
![](../imgs/virtools-mesh.png) ![](../imgs/virtools-mesh.png)

View File

@ -11,6 +11,7 @@ nav:
- 'Install Plugin': 'en/install-plugin.md' - 'Install Plugin': 'en/install-plugin.md'
- 'Configure Plugin': 'en/configure-plugin.md' - 'Configure Plugin': 'en/configure-plugin.md'
- 'Virtools Properties': 'en/virtools-properties.md' - 'Virtools Properties': 'en/virtools-properties.md'
- 'Ballance Properties': 'en/ballance-properties.md'
- 'Import and Export Virtools Document': 'en/import-export-virtools.md' - 'Import and Export Virtools Document': 'en/import-export-virtools.md'
- 'Group Operation': 'en/group-operations.md' - 'Group Operation': 'en/group-operations.md'
- 'Legacy Alignment': 'en/legacy-align.md' - 'Legacy Alignment': 'en/legacy-align.md'
@ -27,6 +28,7 @@ nav:
- '安装插件': 'zh-cn/install-plugin.md' - '安装插件': 'zh-cn/install-plugin.md'
- '配置插件': 'zh-cn/configure-plugin.md' - '配置插件': 'zh-cn/configure-plugin.md'
- 'Virtools属性': 'zh-cn/virtools-properties.md' - 'Virtools属性': 'zh-cn/virtools-properties.md'
- 'Ballance属性': 'zh-cn/ballance-properties.md'
- '导入导出Virtools文档': 'zh-cn/import-export-virtools.md' - '导入导出Virtools文档': 'zh-cn/import-export-virtools.md'
- '按组操作': 'zh-cn/group-operations.md' - '按组操作': 'zh-cn/group-operations.md'
- '传统对齐': 'zh-cn/legacy-align.md' - '传统对齐': 'zh-cn/legacy-align.md'