diff --git a/bbp_ng/OP_ADDS_component.py b/bbp_ng/OP_ADDS_component.py new file mode 100644 index 0000000..ed51dbd --- /dev/null +++ b/bbp_ng/OP_ADDS_component.py @@ -0,0 +1,77 @@ +import bpy +from . import UTIL_functions, UTIL_icons_manager +from . import PROP_preferences, PROP_ballance_element, PROP_virtools_group + +_g_UniqueElements = { + "PS_FourFlames": 'PS_FourFlames_01', + "PE_Balloon": 'PE_Balloon_01' +} + +def _get_component_name(comp_name: str, comp_sector: int) -> str: + return '{}_{:0>2d}_'.format(comp_name, comp_sector) + +class BBP_OT_add_component(bpy.types.Operator): + """Add Element""" + bl_idname = "bbp.add_component" + bl_label = "Add Element" + bl_options = {'UNDO'} + + element_sector: bpy.props.IntProperty( + name = "Sector", + description = "Define which sector the object will be grouped in", + min = 1, max = 999, + soft_min = 1, soft_max = 8, + default = 1, + ) + + element_type: bpy.props.EnumProperty( + name = "Type", + description = "This element type", + #items=tuple(map(lambda x: (x, x, ""), UTILS_constants.bmfile_componentList)), + items = tuple( + # token, display name, descriptions, icon, index + (str(item.value), item.name, "", UTIL_icons_manager.get_element_icon(item.name), item.value) + for item in PROP_ballance_element.BallanceElementType + ), + ) + + def invoke(self, context, event): + wm = context.window_manager + return wm.invoke_props_dialog(self) + + def draw(self, context): + layout = self.layout + layout.prop(self, "element_type") + layout.prop(self, "element_sector") + + # check for unique name and show warning + elename: str | None = _g_UniqueElements.get(PROP_ballance_element.BallanceElementType(int(self.element_type)).name, None) + if elename is not None and elename in bpy.data.objects: + layout.label(f'Warning: {elename} already exist.') + + def execute(self, context): + # create by ballance elements + eletype: PROP_ballance_element.BallanceElementType = PROP_ballance_element.BallanceElementType(int(self.element_type)) + with PROP_ballance_element.BallanceElementsHelper(bpy.context.scene) as creator: + obj = bpy.data.objects.new( + _get_component_name(eletype.name, self.element_sector), + creator.get_element(eletype.value) + ) + UTIL_functions.add_into_scene_and_move_to_cursor(obj) + + return {'FINISHED'} + + @classmethod + def draw_blc_menu(self, layout: bpy.types.UILayout): + for item in PROP_ballance_element.BallanceElementType: + cop = layout.operator( + self.bl_idname, text = item.name, + icon_value = UTIL_icons_manager.get_element_icon(item.name)) + cop.element_type = str(item.value) + +def register(): + # register all classes + bpy.utils.register_class(BBP_OT_add_component) + +def unregister(): + bpy.utils.unregister_class(BBP_OT_add_component) diff --git a/bbp_ng/PROP_ballance_element.py b/bbp_ng/PROP_ballance_element.py index 1143690..0752903 100644 --- a/bbp_ng/PROP_ballance_element.py +++ b/bbp_ng/PROP_ballance_element.py @@ -318,7 +318,7 @@ class BBP_UL_ballance_elements(bpy.types.UIList): def draw_item(self, context, layout: bpy.types.UILayout, data, item: BBP_PG_ballance_element, icon, active_data, active_propname): if item.element_name != "" and item.mesh_ptr is not None: layout.label(text = item.element_name, translate = False) - layout.label(text = item.mesh_ptr, translate = False, icon = 'MESH_DATA') + layout.label(text = item.mesh_ptr.name, translate = False, icon = 'MESH_DATA') class BBP_OT_reset_ballance_elements(bpy.types.Operator): """Reset all Meshes of Loaded Ballance Elements to Original Geometry.""" diff --git a/bbp_ng/PROP_preferences.py b/bbp_ng/PROP_preferences.py index e031d7a..23e9ac6 100644 --- a/bbp_ng/PROP_preferences.py +++ b/bbp_ng/PROP_preferences.py @@ -5,11 +5,11 @@ from . import UTIL_naming_convension class RawPreferences(): cBallanceTextureFolder: typing.ClassVar[str] = "" cNoComponentCollection: typing.ClassVar[str] = "" - cDefaultNamingConvention: typing.ClassVar[int] = UTIL_naming_convension._EnumPropHelper.get_default_naming_identifier() + cDefaultNamingConvention: typing.ClassVar[UTIL_naming_convension.NamingConvention] = UTIL_naming_convension._EnumPropHelper.get_default_naming_identifier() mBallanceTextureFolder: str mNoComponentCollection: str - mDefaultNamingConvention: int + mDefaultNamingConvention: UTIL_naming_convension.NamingConvention def __init__(self, **kwargs): self.mBallanceTextureFolder = kwargs.get("mBallanceTextureFolder", "") diff --git a/bbp_ng/PROP_virtools_group.py b/bbp_ng/PROP_virtools_group.py index ca236d8..a3e6370 100644 --- a/bbp_ng/PROP_virtools_group.py +++ b/bbp_ng/PROP_virtools_group.py @@ -109,14 +109,22 @@ class VirtoolsGroupsHelper(): return True return False + def intersect_groups(self, gnames: set[str]) -> set[str]: + self.__check_valid() + return self.__mGroupsSet.intersection(gnames) + def iterate_groups(self) -> typing.Iterator[str]: self.__check_valid() return iter(self.__mGroupsSet) - def clear_groups(self): + def clear_groups(self) -> None: self.__check_valid() self.__mNoChange = False self.__mGroupsSet.clear() + + def get_count(self) -> int: + self.__check_valid() + return len(self.__mGroupsSet) def __write_to_virtools_groups(self) -> None: groups: bpy.types.CollectionProperty = get_virtools_groups(self.__mAssocObj) diff --git a/bbp_ng/PROP_virtools_material.py b/bbp_ng/PROP_virtools_material.py index 94b7661..302cf5c 100644 --- a/bbp_ng/PROP_virtools_material.py +++ b/bbp_ng/PROP_virtools_material.py @@ -1,7 +1,7 @@ import bpy import typing, enum -from . import UTIL_virtools_types, UTIL_functions -from . import PROP_virtools_texture +from . import UTIL_virtools_types, UTIL_functions, UTIL_ballance_texture, UTIL_file_browser +from . import PROP_virtools_texture, PROP_preferences class RawVirtoolsMaterial(): @@ -616,6 +616,50 @@ class BBP_OT_preset_virtools_material(bpy.types.Operator): preset_virtools_material(mtl, expected_preset) return {'FINISHED'} +class BBP_OT_direct_set_virtools_texture(bpy.types.Operator, UTIL_file_browser.ImportBallanceImage): + """Import and Assign Texture Directly""" + bl_idname = "bbp.direct_set_virtools_texture" + bl_label = "Import and Assign Texture" + bl_options = {'UNDO'} + + @classmethod + def poll(cls, context): + # ballance texture order this + if not PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder(): return False + # we only accept panel executing + if context.material is None: return False + # ok + return True + + def draw(self, context): + pass + + def invoke(self, context, event): + # preset tex folder + self.general_set_filename(PROP_preferences.get_raw_preferences().mBallanceTextureFolder) + return UTIL_file_browser.ImportBallanceImage.invoke(self, context, event) + + def execute(self, context): + # get assoc mtl + mtl: bpy.types.Material = context.material + rawmtl: RawVirtoolsMaterial = get_raw_virtools_material(mtl) + + # import texture according to whether it is ballance texture + texture_filepath: str = self.general_get_filename() + try_filepath: str | None = UTIL_ballance_texture.get_ballance_texture_filename(texture_filepath) + tex: bpy.types.Image + if try_filepath is None: + # load as other texture + tex = UTIL_ballance_texture.load_other_texture(texture_filepath) + else: + # load as ballance texture + tex = UTIL_ballance_texture.load_ballance_texture(try_filepath) + + # assign texture + rawmtl.mTexture = tex + set_raw_virtools_material(mtl, rawmtl) + return {'FINISHED'} + #endregion class BBP_PT_virtools_material(bpy.types.Panel): @@ -655,7 +699,11 @@ class BBP_PT_virtools_material(bpy.types.Panel): layout.separator() layout.label(text="Texture Parameters") - layout.prop(props, 'texture', emboss = True) + # texture prop with direct importing + sublay = layout.row() + sublay.prop(props, 'texture', emboss = True) + sublay.operator(BBP_OT_direct_set_virtools_texture.bl_idname, text = '', icon = 'FILEBROWSER') + # texture detail if props.texture is not None: # have texture, show texture settings and enclosed by a border. boxlayout = layout.box() @@ -694,6 +742,7 @@ def register(): bpy.utils.register_class(BBP_PG_virtools_material) bpy.utils.register_class(BBP_OT_apply_virtools_material) bpy.utils.register_class(BBP_OT_preset_virtools_material) + bpy.utils.register_class(BBP_OT_direct_set_virtools_texture) bpy.utils.register_class(BBP_PT_virtools_material) # add into material metadata @@ -704,6 +753,7 @@ def unregister(): del bpy.types.Material.virtools_material bpy.utils.unregister_class(BBP_PT_virtools_material) + bpy.utils.unregister_class(BBP_OT_direct_set_virtools_texture) bpy.utils.unregister_class(BBP_OT_preset_virtools_material) bpy.utils.unregister_class(BBP_OT_apply_virtools_material) bpy.utils.unregister_class(BBP_PG_virtools_material) diff --git a/bbp_ng/UTIL_file_browser.py b/bbp_ng/UTIL_file_browser.py index 9dbfeee..1009939 100644 --- a/bbp_ng/UTIL_file_browser.py +++ b/bbp_ng/UTIL_file_browser.py @@ -31,6 +31,9 @@ class ImportBallanceImage(bpy_extras.io_utils.ImportHelper): options = {'HIDDEN'} ) + def general_set_filename(self, filename: str) -> None: + self.filepath = filename + def general_get_filename(self) -> str: return self.filepath diff --git a/bbp_ng/UTIL_functions.py b/bbp_ng/UTIL_functions.py index 6ee6d32..a837d75 100644 --- a/bbp_ng/UTIL_functions.py +++ b/bbp_ng/UTIL_functions.py @@ -51,3 +51,13 @@ def message_box(message: tuple[str, ...], title: str, icon: str): layout.label(text=item, translate=False) bpy.context.window_manager.popup_menu(draw, title = title, icon = icon) + +def move_to_cursor(obj: bpy.types.Object): + obj.location = bpy.context.scene.cursor.location + +def add_into_scene_and_move_to_cursor(obj: bpy.types.Object): + move_to_cursor(obj) + + view_layer = bpy.context.view_layer + collection = view_layer.active_layer_collection.collection + collection.objects.link(obj) diff --git a/bbp_ng/UTIL_naming_convension.py b/bbp_ng/UTIL_naming_convension.py index bee296f..4b2bb17 100644 --- a/bbp_ng/UTIL_naming_convension.py +++ b/bbp_ng/UTIL_naming_convension.py @@ -1,5 +1,5 @@ import bpy -import typing, enum +import typing, enum, re from . import UTIL_functions, UTIL_icons_manager from . import PROP_virtools_group @@ -125,19 +125,17 @@ class BallanceObjectInfo(): return cls(basic_type) class _NamingConventionProfile(): - _TNameFct = typing.Callable[[], str] - _TDescFct = typing.Callable[[], str] _TParseFct = typing.Callable[[bpy.types.Object, _RenameErrorReporter | None], BallanceObjectInfo | None] _TSetFct = typing.Callable[[bpy.types.Object,BallanceObjectInfo, _RenameErrorReporter | None], bool] - mNameFct: _TNameFct - mDescFct: _TDescFct + mName: str + mDesc: str mParseFct: _TParseFct mSetFct: _TSetFct - def __init__(self, name_fct: _TNameFct, desc_fct: _TDescFct, parse_fct: _TParseFct, set_fct: _TSetFct): - self.mNameFct = name_fct - self.mDescFct = desc_fct + def __init__(self, name: str, desc: str, parse_fct: _TParseFct, set_fct: _TSetFct): + self.mName = name + self.mDesc = desc self.mParseFct = parse_fct self.mSetFct = set_fct @@ -145,20 +143,214 @@ class _NamingConventionProfile(): #region Naming Convention Declaration +_g_BlcNormalComponents: set[str] = set(( + PROP_virtools_group.VirtoolsGroupsPreset.P_Extra_Life.value, + PROP_virtools_group.VirtoolsGroupsPreset.P_Extra_Point.value, + PROP_virtools_group.VirtoolsGroupsPreset.P_Trafo_Paper.value, + PROP_virtools_group.VirtoolsGroupsPreset.P_Trafo_Stone.value, + PROP_virtools_group.VirtoolsGroupsPreset.P_Trafo_Wood.value, + PROP_virtools_group.VirtoolsGroupsPreset.P_Ball_Paper.value, + PROP_virtools_group.VirtoolsGroupsPreset.P_Ball_Stone.value, + PROP_virtools_group.VirtoolsGroupsPreset.P_Ball_Wood.value, + PROP_virtools_group.VirtoolsGroupsPreset.P_Box.value, + PROP_virtools_group.VirtoolsGroupsPreset.P_Dome.value, + PROP_virtools_group.VirtoolsGroupsPreset.P_Modul_01.value, + PROP_virtools_group.VirtoolsGroupsPreset.P_Modul_03.value, + PROP_virtools_group.VirtoolsGroupsPreset.P_Modul_08.value, + PROP_virtools_group.VirtoolsGroupsPreset.P_Modul_17.value, + PROP_virtools_group.VirtoolsGroupsPreset.P_Modul_18.value, + PROP_virtools_group.VirtoolsGroupsPreset.P_Modul_19.value, + PROP_virtools_group.VirtoolsGroupsPreset.P_Modul_25.value, + PROP_virtools_group.VirtoolsGroupsPreset.P_Modul_26.value, + PROP_virtools_group.VirtoolsGroupsPreset.P_Modul_29.value, + PROP_virtools_group.VirtoolsGroupsPreset.P_Modul_30.value, + PROP_virtools_group.VirtoolsGroupsPreset.P_Modul_34.value, + PROP_virtools_group.VirtoolsGroupsPreset.P_Modul_37.value, + PROP_virtools_group.VirtoolsGroupsPreset.P_Modul_41.value, +)) +_g_BlcUniqueComponents: set[str] = set(( + PROP_virtools_group.VirtoolsGroupsPreset.PS_Levelstart.value, + PROP_virtools_group.VirtoolsGroupsPreset.PE_Levelende.value, + PROP_virtools_group.VirtoolsGroupsPreset.PC_Checkpoints.value, + PROP_virtools_group.VirtoolsGroupsPreset.PR_Resetpoints.value, +)) +_g_BlcFloor: set[str] = set(( + PROP_virtools_group.VirtoolsGroupsPreset.Sound_HitID_01.value, + PROP_virtools_group.VirtoolsGroupsPreset.Sound_RollID_01.value, +)) +_g_BlcWood: set[str] = set(( + PROP_virtools_group.VirtoolsGroupsPreset.Sound_HitID_02.value, + PROP_virtools_group.VirtoolsGroupsPreset.Sound_RollID_02.value, +)) + 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])_.*$') + cRegexPC: typing.ClassVar[re.Pattern] = re.compile('^PC_TwoFlames_(0[1-7])$') + cRegexPR: typing.ClassVar[re.Pattern] = re.compile('^PR_Resetpoint_(0[1-8])$') + + @staticmethod + def __get_pcpr_from_name(name: str, reporter: _RenameErrorReporter | None) -> BallanceObjectInfo | None: + regex_result = _VirtoolsGroupConvention.cRegexPC.match(name) + if regex_result is not None: + return BallanceObjectInfo.create_from_checkpoint( + int(regex_result.group(1)) + ) + regex_result = _VirtoolsGroupConvention.cRegexPR.match(name) + if regex_result is not None: + return BallanceObjectInfo.create_from_resetpoint( + int(regex_result.group(1)) + ) + + if reporter: reporter.add_error("PC_Checkpoints or PR_Resetpoints detected. But couldn't get sector from name.") + return None + + @staticmethod + def __get_sector_from_groups(gps: typing.Iterator[str]) -> int | None: + # this counter is served for stupid + # multi-sector-grouping accident. + counter: int = 0 + last_matched_sector: int = 0 + for i in gps: + regex_result = _VirtoolsGroupConvention.cRegexGroupSector.match(i) + if regex_result is not None: + last_matched_sector = int(regex_result.group(1)) + counter += 1 + + if counter != 1: return None + else: return last_matched_sector + + @staticmethod def parse_from_object(obj: bpy.types.Object, reporter: _RenameErrorReporter | None) -> BallanceObjectInfo | None: - return None + # create visitor + with PROP_virtools_group.VirtoolsGroupsHelper(obj) as gp: + # if no group, we should consider it is decoration or skylayer + if gp.get_count() == 0: + if obj.name == 'SkyLayer': return BallanceObjectInfo.create_from_others(BallanceObjectType.SKYLAYER) + else: return BallanceObjectInfo.create_from_others(BallanceObjectType.DECORATION) + + # try to filter unique elements first + inter_gps: set[str] = gp.intersect_groups(_g_BlcUniqueComponents) + if len(inter_gps) == 1: + # get it + match((tuple(inter_gps))[0]): + case PROP_virtools_group.VirtoolsGroupsPreset.PS_Levelstart.value: + return BallanceObjectInfo.create_from_others(BallanceObjectType.LEVEL_START) + case PROP_virtools_group.VirtoolsGroupsPreset.PE_Levelende.value: + return BallanceObjectInfo.create_from_others(BallanceObjectType.LEVEL_END) + case PROP_virtools_group.VirtoolsGroupsPreset.PC_Checkpoints.value | PROP_virtools_group.VirtoolsGroupsPreset.PR_Resetpoints.value: + # these type's data should be gotten from its name + return _VirtoolsGroupConvention.__get_pcpr_from_name(obj.name, reporter) + case _: + if reporter: reporter.add_error("The match of Unique Component lost.") + return None + elif len(inter_gps) != 0: + if reporter: reporter.add_error("A Multi-grouping Unique Component.") + return None + + # distinguish normal elements + inter_gps = gp.intersect_groups(_g_BlcNormalComponents) + if len(inter_gps) == 1: + # get it + # now try get its sector + gotten_elements: str = (tuple(inter_gps))[0] + gotten_sector: int | None = _VirtoolsGroupConvention.__get_sector_from_groups(gp.iterate_groups()) + if gotten_sector is None: + # fail to get sector + if reporter: reporter.add_error("Component detected. But couldn't get sector from CKGroup data.") + return None + return BallanceObjectInfo.create_from_component( + gotten_elements, + gotten_sector + ) + elif len(inter_gps) != 0: + # must be a weird grouping, report it + if reporter: reporter.add_error("A Multi-grouping Component.") + return None + + # distinguish road + if gp.contain_group(PROP_virtools_group.VirtoolsGroupsPreset.Phys_FloorRails.value): + # rail + return BallanceObjectInfo.create_from_others(BallanceObjectType.RAIL) + elif gp.contain_group(PROP_virtools_group.VirtoolsGroupsPreset.Phys_Floors.value): + # distinguish it between Floor and Wood + floor_result = gp.intersect_groups(_g_BlcFloor) + rail_result = gp.intersect_groups(_g_BlcWood) + if len(floor_result) > 0 and len(rail_result) == 0: + return BallanceObjectInfo.create_from_others(BallanceObjectType.FLOOR) + elif len(floor_result) == 0 and len(rail_result) > 0: + return BallanceObjectInfo.create_from_others(BallanceObjectType.WOOD) + else: + if reporter: reporter.add_warning("Can't distinguish object between Floors and Rails. Suppose it is Floors.") + return BallanceObjectInfo.create_from_others(BallanceObjectType.FLOOR) + elif gp.contain_group(PROP_virtools_group.VirtoolsGroupsPreset.Phys_FloorStopper.value): + return BallanceObjectInfo.create_from_others(BallanceObjectType.STOPPER) + elif gp.contain_group(PROP_virtools_group.VirtoolsGroupsPreset.DepthTestCubes.value): + return BallanceObjectInfo.create_from_others(BallanceObjectType.DEPTH_CUBE) + + # no matched + if reporter: reporter.add_error("Group match lost.") + return None @staticmethod def set_to_object(obj: bpy.types.Object, info: BallanceObjectInfo, reporter: _RenameErrorReporter | None) -> bool: - return False + # create visitor + with PROP_virtools_group.VirtoolsGroupsHelper(obj) as gp: + # match by basic type + match(info.mBasicType): + case BallanceObjectType.DECORATION: pass # decoration do not need group + case BallanceObjectType.SKYLAYER: pass # sky layer do not need group + + case BallanceObjectType.LEVEL_START: + gp.add_group(PROP_virtools_group.VirtoolsGroupsPreset.PS_Levelstart.value) + case BallanceObjectType.LEVEL_END: + gp.add_group(PROP_virtools_group.VirtoolsGroupsPreset.PE_Levelende.value) + case BallanceObjectType.CHECKPOINT: + gp.add_group(PROP_virtools_group.VirtoolsGroupsPreset.PC_Checkpoints.value) + case BallanceObjectType.RESETPOINT: + gp.add_group(PROP_virtools_group.VirtoolsGroupsPreset.PR_Resetpoints.value) + + case BallanceObjectType.DEPTH_CUBE: + gp.add_group(PROP_virtools_group.VirtoolsGroupsPreset.PE_Levelende.value) + + case BallanceObjectType.FLOOR: + gp.add_group(PROP_virtools_group.VirtoolsGroupsPreset.Phys_Floors.value) + gp.add_group(PROP_virtools_group.VirtoolsGroupsPreset.Sound_HitID_01.value) + gp.add_group(PROP_virtools_group.VirtoolsGroupsPreset.Sound_RollID_01.value) + case BallanceObjectType.RAIL: + gp.add_group(PROP_virtools_group.VirtoolsGroupsPreset.Phys_FloorRails.value) + gp.add_group(PROP_virtools_group.VirtoolsGroupsPreset.Sound_HitID_02.value) + gp.add_group(PROP_virtools_group.VirtoolsGroupsPreset.Sound_RollID_02.value) + case BallanceObjectType.WOOD: + gp.add_group(PROP_virtools_group.VirtoolsGroupsPreset.Phys_Floors.value) + gp.add_group(PROP_virtools_group.VirtoolsGroupsPreset.Sound_HitID_03.value) + gp.add_group(PROP_virtools_group.VirtoolsGroupsPreset.Sound_RollID_03.value) + case BallanceObjectType.STOPPER: + gp.add_group(PROP_virtools_group.VirtoolsGroupsPreset.Phys_FloorStopper.value) + + case BallanceObjectType.COMPONENT: + # group into component type + gp.add_group(info.mComponentType) + + # group to sector + if info.mSector == 9: + gp.add_group('Sector_9') + else: + gp.add_group(f'Sector_{info.mSector:0>2d}') + + case _: + if reporter is not None: + reporter.add_error('No matched info.') + return False + + return True @staticmethod def register() -> _NamingConventionProfile: return _NamingConventionProfile( - lambda: 'Virtools Group', - lambda: 'Virtools Group', + 'Virtools Group', + 'Virtools Group', _VirtoolsGroupConvention.parse_from_object, _VirtoolsGroupConvention.set_to_object ) @@ -166,42 +358,210 @@ class _VirtoolsGroupConvention(): class _YYCToolchainConvention(): @staticmethod def parse_from_object(obj: bpy.types.Object, reporter: _RenameErrorReporter | None) -> BallanceObjectInfo | None: + # check component first + regex_result = _VirtoolsGroupConvention.cRegexComponent.match(obj.name) + if regex_result is not None: + return BallanceObjectInfo.create_from_component( + regex_result.group(1), + int(regex_result.group(2)) + ) + + # check PC PR elements + regex_result = _VirtoolsGroupConvention.cRegexPC.match(obj.name) + if regex_result is not None: + return BallanceObjectInfo.create_from_checkpoint( + int(regex_result.group(1)) + ) + regex_result = _VirtoolsGroupConvention.cRegexPR.match(obj.name) + if regex_result is not None: + return BallanceObjectInfo.create_from_resetpoint( + int(regex_result.group(1)) + ) + + # check other unique elements + if obj.name == "PS_FourFlames_01": + return BallanceObjectInfo.create_from_others(BallanceObjectType.LEVEL_START) + if obj.name == "PE_Balloon_01": + return BallanceObjectInfo.create_from_others(BallanceObjectType.LEVEL_END) + + # process floors + if obj.name.startswith("A_Floor"): + return BallanceObjectInfo.create_from_others(BallanceObjectType.FLOOR) + if obj.name.startswith("A_Rail"): + return BallanceObjectInfo.create_from_others(BallanceObjectType.RAIL) + if obj.name.startswith("A_Wood"): + return BallanceObjectInfo.create_from_others(BallanceObjectType.WOOD) + if obj.name.startswith("A_Stopper"): + return BallanceObjectInfo.create_from_others(BallanceObjectType.STOPPER) + + # process others + if obj.name.startswith("DepthCubes"): + return BallanceObjectInfo.create_from_others(BallanceObjectType.DEPTH_CUBE) + if obj.name.startswith("D_"): + return BallanceObjectInfo.create_from_others(BallanceObjectType.DECORATION) + if obj.name == 'SkyLayer': + return BallanceObjectInfo.create_from_others(BallanceObjectType.SKYLAYER) + + if reporter is not None: + reporter.add_error("Name match lost.") return None @staticmethod def set_to_object(obj: bpy.types.Object, info: BallanceObjectInfo, reporter: _RenameErrorReporter | None) -> bool: - return False + match(info.mBasicType): + case BallanceObjectType.DECORATION: + obj.name = 'D_' + case BallanceObjectType.SKYLAYER: + obj.name = 'SkyLayer' + + case BallanceObjectType.LEVEL_START: + obj.name = 'PS_FourFlames_01' + case BallanceObjectType.LEVEL_END: + obj.name = 'PE_Balloon_01' + case BallanceObjectType.CHECKPOINT: + obj.name = f'PR_Resetpoint_{info.mSector:0>2d}' + case BallanceObjectType.RESETPOINT: + obj.name = f'PC_TwoFlames_{info.mSector:0>2d}' + + case BallanceObjectType.DEPTH_CUBE: + obj.name = 'DepthCubes_' + + case BallanceObjectType.FLOOR: + obj.name = 'A_Floor_' + case BallanceObjectType.RAIL: + obj.name = 'A_Wood_' + case BallanceObjectType.WOOD: + obj.name = 'A_Rail_' + case BallanceObjectType.STOPPER: + obj.name = 'A_Stopper_' + + case BallanceObjectType.COMPONENT: + obj.name = '{}_{:0>2d}_'.format( + info.mComponentType, info.mSector) + + case _: + if reporter is not None: + reporter.add_error('No matched info.') + return False + + return True @staticmethod def register() -> _NamingConventionProfile: return _NamingConventionProfile( - lambda: 'YYC Toolchain', - lambda: 'YYC Toolchain name standard.', + 'YYC Toolchain', + 'YYC Toolchain name standard.', _YYCToolchainConvention.parse_from_object, _YYCToolchainConvention.set_to_object ) class _ImengyuConvention(): + cRegexComponent: typing.ClassVar[re.Pattern] = re.compile('^(' + '|'.join(_g_BlcNormalComponents) + '):[^:]*:([1-9]|[1-9][0-9])$') + cRegexPC: typing.ClassVar[re.Pattern] = re.compile('^PC_CheckPoint:([0-9]+)$') + cRegexPR: typing.ClassVar[re.Pattern] = re.compile('^PR_ResetPoint:([0-9]+)$') + @staticmethod def parse_from_object(obj: bpy.types.Object, reporter: _RenameErrorReporter | None) -> BallanceObjectInfo | None: + # check component first + regex_result = _ImengyuConvention.cRegexComponent.match(obj.name) + if regex_result is not None: + return BallanceObjectInfo.create_from_component( + regex_result.group(1), + int(regex_result.group(2)) + ) + + # check PC PR elements + regex_result = _ImengyuConvention.cRegexPC.match(obj.name) + if regex_result is not None: + return BallanceObjectInfo.create_from_checkpoint( + int(regex_result.group(1)) + ) + regex_result = _ImengyuConvention.cRegexPR.match(obj.name) + if regex_result is not None: + return BallanceObjectInfo.create_from_resetpoint( + int(regex_result.group(1)) + ) + + # check other unique elements + if obj.name == "PS_LevelStart": + return BallanceObjectInfo.create_from_others(BallanceObjectType.LEVEL_START) + if obj.name == "PE_LevelEnd": + return BallanceObjectInfo.create_from_others(BallanceObjectType.LEVEL_END) + + # process floors + if obj.name.startswith("S_Floors"): + return BallanceObjectInfo.create_from_others(BallanceObjectType.FLOOR) + if obj.name.startswith("S_FloorRails"): + return BallanceObjectInfo.create_from_others(BallanceObjectType.RAIL) + if obj.name.startswith("S_FloorWoods"): + return BallanceObjectInfo.create_from_others(BallanceObjectType.WOOD) + if obj.name.startswith("S_FloorStopper"): + return BallanceObjectInfo.create_from_others(BallanceObjectType.STOPPER) + + # process others + if obj.name.startswith("DepthTestCubes"): + return BallanceObjectInfo.create_from_others(BallanceObjectType.DEPTH_CUBE) + if obj.name.startswith("O_"): + return BallanceObjectInfo.create_from_others(BallanceObjectType.DECORATION) + if obj.name == 'SkyLayer': + return BallanceObjectInfo.create_from_others(BallanceObjectType.SKYLAYER) + + if reporter is not None: + reporter.add_error("Name match lost.") return None @staticmethod def set_to_object(obj: bpy.types.Object, info: BallanceObjectInfo, reporter: _RenameErrorReporter | None) -> bool: - return False + match(info.mBasicType): + case BallanceObjectType.DECORATION: + obj.name = 'O_' + case BallanceObjectType.SKYLAYER: + obj.name = 'SkyLayer' + + case BallanceObjectType.LEVEL_START: + obj.name = 'PS_LevelStart' + case BallanceObjectType.LEVEL_END: + obj.name = 'PE_LevelEnd' + case BallanceObjectType.CHECKPOINT: + obj.name = f'PR_ResetPoint:{info.mSector:d}' + case BallanceObjectType.RESETPOINT: + obj.name = f'PC_CheckPoint:{info.mSector:d}' + + case BallanceObjectType.DEPTH_CUBE: + obj.name = 'DepthTestCubes' + + case BallanceObjectType.FLOOR: + obj.name = 'S_Floors' + case BallanceObjectType.RAIL: + obj.name = 'S_FloorWoods' + case BallanceObjectType.WOOD: + obj.name = 'S_FloorRails' + case BallanceObjectType.STOPPER: + obj.name = 'S_FloorStopper' + + case BallanceObjectType.COMPONENT: + obj.name = '{}:{}:{:d}'.format( + info.mComponentType, obj.name.replace(':', '_'), info.mSector) + + case _: + if reporter is not None: + reporter.add_error('No matched info.') + return False + + return True @staticmethod def register() -> _NamingConventionProfile: return _NamingConventionProfile( - lambda: 'Imengyu Ballance', - lambda: 'Auto grouping name standard for Imengyu/Ballance.', + 'Imengyu Ballance', + 'Auto grouping name standard for Imengyu/Ballance.', _ImengyuConvention.parse_from_object, _ImengyuConvention.set_to_object ) #endregion -#region Nameing Convention Register +#region Naming Convention Register ## All available naming conventions # Each naming convention should have a identifier for visiting them. @@ -213,19 +573,11 @@ def _register_naming_convention_with_index(profile: _NamingConventionProfile) -> _g_NamingConventions.append(profile) return ret -# register native and default one and others -# but only native one and default one need keep its index -# -# The native naming convention is Virtools Group -# We treat it as naming convention because we want use a universal interface to process naming converting. -# So Virtools Group can no be seen as a naming convention, but we treat it like naming convention in code. -# The "native" mean this is -# -# The default fallback naming convention is YYC toolchain -# -_g_NativeNamingConventionIndex: int = _register_naming_convention_with_index(_VirtoolsGroupConvention.register()) -_g_DefaultNamingConventionIndex: int = _register_naming_convention_with_index(_YYCToolchainConvention.register()) -_register_naming_convention_with_index(_ImengyuConvention.register()) +# register and assign to a enum +class NamingConvention(enum.IntEnum): + VirtoolsGroup = _register_naming_convention_with_index(_VirtoolsGroupConvention.register()) + YYCToolchain = _register_naming_convention_with_index(_YYCToolchainConvention.register()) + Imengyu = _register_naming_convention_with_index(_ImengyuConvention.register()) class _EnumPropHelper(): """ @@ -238,52 +590,56 @@ class _EnumPropHelper(): # create a function to filter Virtools Group profile # and return index at the same time def naming_convention_iter() -> typing.Iterator[tuple[int, _NamingConventionProfile]]: - for idx, item in enumerate(_g_NamingConventions): - if idx != _g_NativeNamingConventionIndex: - yield (idx, item) + for item in NamingConvention: + if item != NamingConvention.VirtoolsGroup: + yield (item.value, _g_NamingConventions[item.value]) # token, display name, descriptions, icon, index return tuple( ( str(idx), - item.mNameFct(), - item.mDescFct(), + item.mName, + item.mDesc, "", idx ) for idx, item in naming_convention_iter() ) @staticmethod - def get_selection(prop: str) -> int: - return int(prop) + def get_selection(prop: str) -> NamingConvention: + return NamingConvention(int(prop)) @staticmethod - def to_selection(val: int) -> str: - return str(val) + def to_selection(val: NamingConvention) -> str: + return str(val.value) @staticmethod - def get_virtools_group_identifier() -> int: - return _g_NativeNamingConventionIndex + def get_virtools_group_identifier() -> NamingConvention: + # The native naming convention is Virtools Group + # We treat it as naming convention because we want use a universal interface to process naming converting. + # So Virtools Group can no be seen as a naming convention, but we treat it like naming convention in code. + return NamingConvention.VirtoolsGroup @staticmethod - def get_default_naming_identifier() -> int: - return _g_DefaultNamingConventionIndex + def get_default_naming_identifier() -> NamingConvention: + # The default fallback naming convention is YYC toolchain + return NamingConvention.YYCToolchain #endregion -def name_setter_core(ident: int, info: BallanceObjectInfo, obj: bpy.types.Object) -> None: +def name_setter_core(ident: NamingConvention, info: BallanceObjectInfo, obj: bpy.types.Object) -> None: # get profile - profile: _NamingConventionProfile = _g_NamingConventions[ident] + profile: _NamingConventionProfile = _g_NamingConventions[ident.value] # set name. don't care whether success. profile.mSetFct(obj, info, None) -def name_converting_core(src_ident: int, dst_ident: int, objs: typing.Iterable[bpy.types.Object]) -> None: +def name_converting_core(src_ident: NamingConvention, dst_ident: NamingConvention, objs: typing.Iterable[bpy.types.Object]) -> None: # no convert needed if src_ident == dst_ident: return # get convert profile - src: _NamingConventionProfile = _g_NamingConventions[src_ident] - dst: _NamingConventionProfile = _g_NamingConventions[dst_ident] + src: _NamingConventionProfile = _g_NamingConventions[src_ident.value] + dst: _NamingConventionProfile = _g_NamingConventions[dst_ident.value] # create reporter and success counter failed_obj_counter: int = 0 @@ -327,4 +683,3 @@ def name_converting_core(src_ident: int, dst_ident: int, objs: typing.Iterable[b "Rename Report", UTIL_icons_manager.BlenderPresetIcons.Info.value ) - diff --git a/bbp_ng/__init__.py b/bbp_ng/__init__.py index 7ce29c5..3dd18a4 100644 --- a/bbp_ng/__init__.py +++ b/bbp_ng/__init__.py @@ -32,6 +32,7 @@ UTIL_icons_manager.register() from . import PROP_preferences, PROP_ptrprop_resolver, PROP_virtools_material, PROP_virtools_texture, PROP_virtools_mesh, PROP_ballance_element, PROP_virtools_group 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_ADDS_component #region Menu @@ -47,6 +48,43 @@ class BBP_MT_View3DMenu(bpy.types.Menu): layout.operator(OP_UV_flatten_uv.BBP_OT_flatten_uv.bl_idname) layout.operator(OP_UV_rail_uv.BBP_OT_rail_uv.bl_idname) +class BBP_MT_AddFloorMenu(bpy.types.Menu): + """Add Ballance Floor""" + bl_idname = "BBP_MT_AddFloorMenu" + bl_label = "Floors" + + def draw(self, context): + layout = self.layout + + layout.label(text="Basic floor") + + layout.separator() + layout.label(text="Derived floor") +class BBP_MT_AddRailMenu(bpy.types.Menu): + """Add Ballance Rail""" + bl_idname = "BBP_MT_AddRailMenu" + bl_label = "Rails" + + def draw(self, context): + layout = self.layout +class BBP_MT_AddElementsMenu(bpy.types.Menu): + """Add Ballance Elements""" + bl_idname = "BBP_MT_AddElementsMenu" + bl_label = "Elements" + def draw(self, context): + layout = self.layout + + layout.label(text="Basic Elements") + OP_ADDS_component.BBP_OT_add_component.draw_blc_menu(layout) + + layout.separator() + layout.label(text="Duplicated Elements") + #OBJS_add_components.BBP_OT_add_components_dup.draw_blc_menu(layout) + + layout.separator() + layout.label(text="Elements Pair") + #OBJS_add_components.BBP_OT_add_components_series.draw_blc_menu(layout) + # ===== Menu Drawer ===== MenuDrawer_t = typing.Callable[[typing.Any, typing.Any], None] @@ -65,12 +103,25 @@ def menu_drawer_view3d(self, context): layout: bpy.types.UILayout = self.layout layout.menu(BBP_MT_View3DMenu.bl_idname) +def menu_drawer_add(self, context): + layout: bpy.types.UILayout = self.layout + layout.separator() + layout.label(text="Ballance") + layout.menu(BBP_MT_AddFloorMenu.bl_idname, icon='MESH_CUBE') + layout.menu(BBP_MT_AddRailMenu.bl_idname, icon='MESH_CIRCLE') + layout.menu(BBP_MT_AddElementsMenu.bl_idname, icon='MESH_ICOSPHERE') + #layout.operator_menu_enum( + # OBJS_add_components.BALLANCE_OT_add_components.bl_idname, + # "elements_type", icon='MESH_ICOSPHERE', text="Elements") #endregion #region Register and Unregister. g_BldClasses: tuple[typing.Any, ...] = ( BBP_MT_View3DMenu, + BBP_MT_AddFloorMenu, + BBP_MT_AddRailMenu, + BBP_MT_AddElementsMenu ) class MenuEntry(): @@ -86,6 +137,7 @@ g_BldMenus: tuple[MenuEntry, ...] = ( MenuEntry(bpy.types.VIEW3D_MT_editor_menus, False, menu_drawer_view3d), MenuEntry(bpy.types.TOPBAR_MT_file_import, True, menu_drawer_import), MenuEntry(bpy.types.TOPBAR_MT_file_export, True, menu_drawer_export), + MenuEntry(bpy.types.VIEW3D_MT_add, True, menu_drawer_add), ) def register() -> None: @@ -106,6 +158,7 @@ def register() -> None: OP_UV_rail_uv.register() OP_UV_flatten_uv.register() + OP_ADDS_component.register() # register other classes for cls in g_BldClasses: @@ -128,6 +181,7 @@ def unregister() -> None: bpy.utils.unregister_class(cls) # unregister modules + OP_ADDS_component.unregister() OP_UV_flatten_uv.unregister() OP_UV_rail_uv.unregister()