import bpy import enum, typing from . import UTIL_virtools_types, UTIL_functions from . import PROP_ptrprop_resolver, PROP_ballance_map_info ## Intent # Some importer or exporter may share same properties. # So we create some shared class and user just need inherit them # and call general getter to get user selected data. # Also provide draw function thus caller do not need draw the params themselves. class ConflictStrategy(enum.IntEnum): Rename = enum.auto() Current = enum.auto() _g_ConflictStrategyDesc: dict[ConflictStrategy, tuple[str, str]] = { ConflictStrategy.Rename: ('Rename', 'Rename the new one'), ConflictStrategy.Current: ('Use Current', 'Use current one'), } _g_EnumHelper_ConflictStrategy: UTIL_functions.EnumPropHelper = UTIL_functions.EnumPropHelper( ConflictStrategy, lambda x: str(x.value), lambda x: ConflictStrategy(int(x)), lambda x: _g_ConflictStrategyDesc[x][0], lambda x: _g_ConflictStrategyDesc[x][1], lambda _: '' ) #region Assist Classes class ExportEditModeBackup(): """ The class which save Edit Mode when exporting and restore it after exporting. Because edit mode is not allowed when exporting. Support `with` statement. ``` with ExportEditModeBackup(): # do some exporting work blabla() # restore automatically when exiting "with" ``` """ mInEditMode: bool def __init__(self): if bpy.context.object and bpy.context.object.mode == "EDIT": # set and toggle it. otherwise exporting will failed. self.mInEditMode = True bpy.ops.object.editmode_toggle() else: self.mInEditMode = False def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): if self.mInEditMode: bpy.ops.object.editmode_toggle() self.mInEditMode = False class ConflictResolver(): """ This class frequently used when importing objects. This class accept 4 conflict strategies for object, mesh, material and texture, and provide 4 general creation functions to handle these strategies. Each general creation functions will return an instance and a bool indicating whether this instance need be initialized. """ __mObjectStrategy: ConflictStrategy __mMeshStrategy: ConflictStrategy __mMaterialStrategy: ConflictStrategy __mTextureStrategy: ConflictStrategy def __init__(self, obj_strategy: ConflictStrategy, mesh_strategy: ConflictStrategy, mtl_strategy: ConflictStrategy, tex_strategy: ConflictStrategy): self.__mObjectStrategy = obj_strategy self.__mMeshStrategy = mesh_strategy self.__mMaterialStrategy = mtl_strategy self.__mTextureStrategy = tex_strategy def create_object(self, name: str, data: bpy.types.Mesh) -> tuple[bpy.types.Object, bool]: """ Create object according to conflict strategy. `data` will only be applied when creating new object (no existing instance or strategy order rename) """ if self.__mObjectStrategy == ConflictStrategy.Current: old: bpy.types.Object | None = bpy.data.objects.get(name, None) if old is not None: return (old, False) return (bpy.data.objects.new(name, data), True) def create_mesh(self, name: str) -> tuple[bpy.types.Mesh, bool]: if self.__mMeshStrategy == ConflictStrategy.Current: old: bpy.types.Mesh | None = bpy.data.meshes.get(name, None) if old is not None: return (old, False) return (bpy.data.meshes.new(name), True) def create_material(self, name: str) -> tuple[bpy.types.Material, bool]: if self.__mMaterialStrategy == ConflictStrategy.Current: old: bpy.types.Material | None = bpy.data.materials.get(name, None) if old is not None: return (old, False) return (bpy.data.materials.new(name), True) def create_texture(self, name: str, fct_cret: typing.Callable[[], bpy.types.Image]) -> tuple[bpy.types.Image, bool]: """ Create texture according to conflict strategy. If the strategy order current, it will return current existing instance. If no existing instance or strategy order rename, it will call `fct_cret` to create new texture. Because texture do not have a general creation function, we frequently create it by other modules provided texture functions. So `fct_cret` is the real creation function. And it will not be executed if no creation happended. """ if self.__mTextureStrategy == ConflictStrategy.Current: old: bpy.types.Image | None = bpy.data.images.get(name, None) if old is not None: return (old, False) # create texture, set name, and return tex: bpy.types.Image = fct_cret() tex.name = name return (tex, True) #endregion class ImportParams(): texture_conflict_strategy: bpy.props.EnumProperty( name = "Texture Name Conflict", items = _g_EnumHelper_ConflictStrategy.generate_items(), description = "Define how to process texture name conflict", default = _g_EnumHelper_ConflictStrategy.to_selection(ConflictStrategy.Current), ) # type: ignore material_conflict_strategy: bpy.props.EnumProperty( name = "Material Name Conflict", items = _g_EnumHelper_ConflictStrategy.generate_items(), description = "Define how to process material name conflict", default = _g_EnumHelper_ConflictStrategy.to_selection(ConflictStrategy.Rename), ) # type: ignore mesh_conflict_strategy: bpy.props.EnumProperty( name = "Mesh Name Conflict", items = _g_EnumHelper_ConflictStrategy.generate_items(), description = "Define how to process mesh name conflict", default = _g_EnumHelper_ConflictStrategy.to_selection(ConflictStrategy.Rename), ) # type: ignore object_conflict_strategy: bpy.props.EnumProperty( name = "Object Name Conflict", items = _g_EnumHelper_ConflictStrategy.generate_items(), description = "Define how to process object name conflict", default = _g_EnumHelper_ConflictStrategy.to_selection(ConflictStrategy.Rename), ) # type: ignore def draw_import_params(self, layout: bpy.types.UILayout) -> None: header: bpy.types.UILayout body: bpy.types.UILayout header, body = layout.panel("BBP_PT_ioport_shared_import_params", default_closed=False) header.label(text = 'Import Parameters') if body is None: return body.label(text = 'Object Name Conflict') body.prop(self, 'object_conflict_strategy', text = '') body.label(text = 'Mesh Name Conflict') body.prop(self, 'mesh_conflict_strategy', text = '') body.label(text = 'Material Name Conflict') body.prop(self, 'material_conflict_strategy', text = '') body.label(text = 'Texture Name Conflict') body.prop(self, 'texture_conflict_strategy', text = '') def general_get_texture_conflict_strategy(self) -> ConflictStrategy: return _g_EnumHelper_ConflictStrategy.get_selection(self.texture_conflict_strategy) def general_get_material_conflict_strategy(self) -> ConflictStrategy: return _g_EnumHelper_ConflictStrategy.get_selection(self.material_conflict_strategy) def general_get_mesh_conflict_strategy(self) -> ConflictStrategy: return _g_EnumHelper_ConflictStrategy.get_selection(self.mesh_conflict_strategy) def general_get_object_conflict_strategy(self) -> ConflictStrategy: return _g_EnumHelper_ConflictStrategy.get_selection(self.object_conflict_strategy) def general_get_conflict_resolver(self) -> ConflictResolver: return ConflictResolver( self.general_get_object_conflict_strategy(), self.general_get_mesh_conflict_strategy(), self.general_get_material_conflict_strategy(), self.general_get_texture_conflict_strategy() ) class ExportParams(): export_mode: bpy.props.EnumProperty( name = "Export Mode", items = ( ('COLLECTION', "Collection", "Export a collection", 'OUTLINER_COLLECTION', 0), ('OBJECT', "Object", "Export an object", 'OBJECT_DATA', 1), ), ) # type: ignore def draw_export_params(self, layout: bpy.types.UILayout) -> None: header: bpy.types.UILayout body: bpy.types.UILayout header, body = layout.panel("BBP_PT_ioport_shared_export_params", default_closed=False) header.label(text = 'Export Parameters') if body is None: return # make prop expand horizontaly, not vertical. horizon_body = body.row() # draw switch horizon_body.prop(self, "export_mode", expand = True) # draw picker if self.export_mode == 'COLLECTION': PROP_ptrprop_resolver.draw_export_collection(body) elif self.export_mode == 'OBJECT': PROP_ptrprop_resolver.draw_export_object(body) def general_get_export_objects(self) -> tuple[bpy.types.Object] | None: """ Return resolved exported objects or None if no selection. """ if self.export_mode == 'COLLECTION': col: bpy.types.Collection = PROP_ptrprop_resolver.get_export_collection() if col is None: return None else: return tuple(col.all_objects) else: obj: bpy.types.Object = PROP_ptrprop_resolver.get_export_object() if obj is None: return None else: return (obj, ) # define global tex save opt blender enum prop helper _g_EnumHelper_CK_TEXTURE_SAVEOPTIONS: UTIL_virtools_types.EnumPropHelper = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.CK_TEXTURE_SAVEOPTIONS) class VirtoolsParams(): vt_encodings: bpy.props.StringProperty( name = "Encodings", description = "The encoding list used by Virtools engine to resolve object name. Use `;` to split multiple encodings", default = UTIL_virtools_types.g_PyBMapDefaultEncoding ) # type: ignore texture_save_opt: bpy.props.EnumProperty( name = "Global Texture 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(), default = _g_EnumHelper_CK_TEXTURE_SAVEOPTIONS.to_selection(UTIL_virtools_types.CK_TEXTURE_SAVEOPTIONS.CKTEXTURE_EXTERNAL) ) # type: ignore use_compress: bpy.props.BoolProperty( name="Use Compress", description = "Whether use ZLib to compress result when saving composition.", default = True, ) # type: ignore compress_level: bpy.props.IntProperty( name = "Compress Level", description = "The ZLib compress level used by Virtools Engine when saving composition.", min = 1, max = 9, default = 5, ) # type: ignore def draw_virtools_params(self, layout: bpy.types.UILayout, is_importer: bool) -> None: header: bpy.types.UILayout body: bpy.types.UILayout header, body = layout.panel("BBP_PT_ioport_shared_virtools_params", default_closed=False) header.label(text = 'Virtools Parameters') if body is None: return # draw encodings body.label(text = 'Encodings') body.prop(self, 'vt_encodings', text = '') # following field are only valid in exporter if not is_importer: body.separator() body.label(text = 'Global Texture Save Options') body.prop(self, 'texture_save_opt', text = '') body.separator() body.prop(self, 'use_compress') if self.use_compress: body.prop(self, 'compress_level') def general_get_vt_encodings(self) -> tuple[str]: # get encoding, split it by `;` and strip blank chars. encodings: str = self.vt_encodings return tuple(map(lambda x: x.strip(), encodings.split(';'))) def general_get_texture_save_opt(self) -> UTIL_virtools_types.CK_TEXTURE_SAVEOPTIONS: return _g_EnumHelper_CK_TEXTURE_SAVEOPTIONS.get_selection(self.texture_save_opt) def general_get_use_compress(self) -> bool: return self.use_compress def general_get_compress_level(self) -> int: return self.compress_level class BallanceParams(): 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 def draw_ballance_params(self, layout: bpy.types.UILayout, is_importer: bool) -> None: # ballance params only presented in exporter. # so if we are in impoerter, we skip the whole function # because we don't want to create an empty panel. if is_importer: return header: bpy.types.UILayout body: bpy.types.UILayout header, body = layout.panel("BBP_PT_ioport_shared_ballance_params", default_closed=False) header.label(text = 'Ballance Parameters') if body is None: return map_info: PROP_ballance_map_info.RawBallanceMapInfo = PROP_ballance_map_info.get_raw_ballance_map_info(bpy.context.scene) body.prop(self, 'successive_sector') body.label(text = f'Map Sectors: {map_info.mSectorCount}') def general_get_successive_sector(self) -> bool: return self.successive_sector def general_get_successive_sector_count(self) -> int: # if user do not pick successive sector, return a random int directly. if not self.general_get_successive_sector(): return 0 # otherwise fetch user specified sector number map_info: PROP_ballance_map_info.RawBallanceMapInfo map_info = PROP_ballance_map_info.get_raw_ballance_map_info(bpy.context.scene) return map_info.mSectorCount