diff --git a/README.md b/README.md index ae7cf9a..9e500d6 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ / ___ \| |_) | |___ ___) | |_| | /_/ \_\____/ \____|____/|____/ ``` -v0.0.7 +v0.0.4 Convenience tool to work with 3D typography in Blender and Cinema4D. diff --git a/__init__.py b/__init__.py index b866537..b3ca721 100644 --- a/__init__.py +++ b/__init__.py @@ -4,25 +4,24 @@ A 3D font helper """ -import importlib import os - -import bpy from bpy.app.handlers import persistent - -from . import addon_updater_ops, bimport, butils -from .common import Font, utils +from bpy.types import Panel +import functools +import io +import bpy +import importlib bl_info = { "name": "ABC3D", "author": "Jakob Schlötter, Studio Pointer*", - "version": (0, 0, 7), + "version": (0, 0, 4), "blender": (4, 1, 0), "location": "VIEW3D", "description": "Convenience addon for 3D fonts", "category": "Typography", } -# NOTE: also change version in common/utils.py and README.md +# NOTE: also change version in common/utils.py # make sure that modules are reloadable # when registering @@ -34,6 +33,12 @@ if "Font" in locals(): importlib.reload(butils) importlib.reload(bimport) importlib.reload(addon_updater_ops) +else: + from .common import Font + from .common import utils + from . import butils + from . import bimport + from . import addon_updater_ops def getPreferences(context): @@ -53,68 +58,60 @@ class ABC3D_addonPreferences(bpy.types.AddonPreferences): auto_check_update = bpy.props.BoolProperty( name="Auto-check for Update", description="If enabled, auto-check for updates using an interval", - default=False, - ) + default=False) updater_interval_months = bpy.props.IntProperty( - name="Months", + name='Months', description="Number of months between checking for updates", default=0, - min=0, - ) + min=0) updater_interval_days = bpy.props.IntProperty( - name="Days", + name='Days', description="Number of days between checking for updates", default=7, min=0, - max=31, - ) + max=31) updater_interval_hours = bpy.props.IntProperty( - name="Hours", + name='Hours', description="Number of hours between checking for updates", default=0, min=0, - max=23, - ) + max=23) updater_interval_minutes = bpy.props.IntProperty( - name="Minutes", + name='Minutes', description="Number of minutes between checking for updates", default=0, min=0, - max=59, - ) + max=59) def get_default_assets_dir(): - return bpy.utils.user_resource("DATAFILES", path=f"{__name__}", create=True) + return bpy.utils.user_resource( + 'DATAFILES', + path=f"{__name__}", + create=True) def on_change_assets_dir(self, context): if not os.path.isdir(self.assets_dir): butils.ShowMessageBox( title=f"{__name__} Warning", icon="ERROR", - message=( - "Chosen directory does not exist.", - "Please, reset to default, create it or chose another one.", - ), - ) + message=("Chosen directory does not exist.", + "Please, reset to default, create it or chose another one.")) elif not os.access(self.assets_dir, os.W_OK): butils.ShowMessageBox( title=f"{__name__} Warning", icon="ERROR", - message=( - "Chosen directory is not writable.", - "Please reset to default or chose another one.", - ), - ) + message=("Chosen directory is not writable.", + "Please reset to default or chose another one.")) print(f"{__name__}: change assets_dir to {self.assets_dir}") assets_dir: bpy.props.StringProperty( name="Assets Folder", - subtype="DIR_PATH", + subtype='DIR_PATH', default=get_default_assets_dir(), update=on_change_assets_dir, ) @@ -124,6 +121,10 @@ class ABC3D_addonPreferences(bpy.types.AddonPreferences): layout.label(text="Directory for storage of fonts and other assets:") layout.prop(self, "assets_dir") + # Works best if a column, or even just self.layout. + mainrow = layout.row() + col = mainrow.column() + # Updater draw function, could also pass in col as third arg. addon_updater_ops.update_settings_ui(self, context) @@ -134,29 +135,16 @@ class ABC3D_available_font(bpy.types.PropertyGroup): class ABC3D_glyph_properties(bpy.types.PropertyGroup): - - def update_callback(self, context): - if self.text_id >= 0: - butils.set_text_on_curve( - context.scene.abc3d_data.available_texts[self.text_id] - ) - glyph_id: bpy.props.StringProperty(maxlen=1) - text_id: bpy.props.IntProperty( - default=-1, - ) - alternate: bpy.props.IntProperty( - default=-1, - update=update_callback, - ) glyph_object: bpy.props.PointerProperty(type=bpy.types.Object) letter_spacing: bpy.props.FloatProperty( name="Letter Spacing", description="Letter Spacing", - update=update_callback, ) + class ABC3D_text_properties(bpy.types.PropertyGroup): + def font_items_callback(self, context): items = [] for f in Font.get_loaded_fonts_and_faces(): @@ -165,6 +153,7 @@ class ABC3D_text_properties(bpy.types.PropertyGroup): def font_default_callback(self, context): d = context.scene.abc3d_data + items = self.font_items_callback(context) if len(d.available_fonts) > 0: if len(d.available_fonts) > d.active_text_index: f = d.available_fonts[d.active_text_index] @@ -173,20 +162,19 @@ class ABC3D_text_properties(bpy.types.PropertyGroup): f = d.available_fonts[0] return 0 # f"{f.font_name} {f.face_name}" - if not isinstance(self.font_name, None) and not isinstance(self.face_name, None): + if type(self.font_name) != type(None) and type(self.face_name) != type(None): return 0 # f"{self.font_name} {self.face_name}" else: return 0 # "" def glyphs_update_callback(self, context): - butils.prepare_text(self.font_name, self.face_name, self.text) - butils.set_text_on_curve(self, can_regenerate=True) + butils.prepare_text(self.font_name, + self.face_name, + self.text) + butils.set_text_on_curve(self) def update_callback(self, context): - try: - butils.set_text_on_curve(self) - except (AttributeError, TypeError): - butils.set_text_on_curve(self, can_regenerate=True) + butils.set_text_on_curve(self) def font_update_callback(self, context): font_name, face_name = self.font.split(" ") @@ -199,40 +187,46 @@ class ABC3D_text_properties(bpy.types.PropertyGroup): items=font_items_callback, update=font_update_callback, ) - font_name: bpy.props.StringProperty(update=glyphs_update_callback) - face_name: bpy.props.StringProperty(update=glyphs_update_callback) + font_name: bpy.props.StringProperty( + update=glyphs_update_callback + ) + face_name: bpy.props.StringProperty( + update=glyphs_update_callback + ) text_object: bpy.props.PointerProperty(type=bpy.types.Object) - text: bpy.props.StringProperty(update=glyphs_update_callback) + text: bpy.props.StringProperty( + update=glyphs_update_callback + ) letter_spacing: bpy.props.FloatProperty( update=update_callback, name="Letter Spacing", description="Letter Spacing", - options={"ANIMATABLE"}, + options={'ANIMATABLE'}, step=0.01, ) orientation: bpy.props.FloatVectorProperty( update=update_callback, name="Orientation", default=(1.5707963267948966, 0.0, 0.0), # 90 degrees in radians - subtype="EULER", + subtype='EULER', ) translation: bpy.props.FloatVectorProperty( update=update_callback, name="Translation", default=(0.0, 0.0, 0.0), - subtype="TRANSLATION", + subtype='TRANSLATION', ) font_size: bpy.props.FloatProperty( update=update_callback, name="Font Size", default=1.0, - subtype="NONE", + subtype='NONE', ) offset: bpy.props.FloatProperty( update=update_callback, name="Offset", default=0.0, - subtype="NONE", + subtype='NONE', ) compensate_curvature: bpy.props.BoolProperty( update=update_callback, @@ -245,26 +239,12 @@ class ABC3D_text_properties(bpy.types.PropertyGroup): name="Ignore Curve Orientation", default=False, ) - loop_in: bpy.props.BoolProperty( - update=update_callback, - name="Loop In", - description="Loop letter on curve if negative offset would place it in front of it.", - default=False, - ) - loop_out: bpy.props.BoolProperty( - update=update_callback, - name="Loop Out", - description="Loop letter on curve if a large offset would place it behind it.", - default=False, - ) distribution_type: bpy.props.StringProperty() glyphs: bpy.props.CollectionProperty(type=ABC3D_glyph_properties) - class ABC3D_data(bpy.types.PropertyGroup): available_fonts: bpy.props.CollectionProperty( - type=ABC3D_available_font, name="Available fonts" - ) + type=ABC3D_available_font, name="Available fonts") def active_font_index_update(self, context): if len(self.available_fonts) <= self.active_font_index: @@ -275,8 +255,7 @@ class ABC3D_data(bpy.types.PropertyGroup): update=active_font_index_update, ) available_texts: bpy.props.CollectionProperty( - type=ABC3D_text_properties, name="Available texts" - ) + type=ABC3D_text_properties, name="Available texts") def active_text_index_update(self, context): if self.active_text_index != -1: @@ -284,15 +263,12 @@ class ABC3D_data(bpy.types.PropertyGroup): # active_text_index changed. so let's update the selection # check if it is already selected # or perhaps one of the glyphs - if ( - not o.select_get() - and not len([c for c in o.children if c.select_get()]) > 0 - ): + if not o.select_get() and not len([c for c in o.children if c.select_get()]) > 0: bpy.ops.object.select_all(action="DESELECT") o.select_set(True) bpy.context.view_layer.objects.active = o # else: - # print("already selected") + # print("already selected") active_text_index: bpy.props.IntProperty(update=active_text_index_update) @@ -307,19 +283,15 @@ class ABC3D_data(bpy.types.PropertyGroup): default="", maxlen=1024, # update=font_path_update_callback, - subtype="FILE_PATH", - ) + subtype="FILE_PATH") export_dir: bpy.props.StringProperty( name="Export Directory", - description="The directory in which we will export fonts.\nIf it is blank, we will export to the addon assets path.\nThis is where the fonts are installed.", - subtype="DIR_PATH", - ) + description=f"The directory in which we will export fonts.\nIf it is blank, we will export to the addon assets path.\nThis is where the fonts are installed.", + subtype="DIR_PATH") class ABC3D_UL_fonts(bpy.types.UIList): - def draw_item( - self, context, layout, data, item, icon, active_data, active_propname, index - ): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): # avoids renaming the item by accident layout.label(text=f"{index}: {item.font_name} {item.face_name}") @@ -328,9 +300,7 @@ class ABC3D_UL_fonts(bpy.types.UIList): class ABC3D_UL_texts(bpy.types.UIList): - def draw_item( - self, context, layout, data, item, icon, active_data, active_propname, index - ): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): split = layout.split(factor=0.3) split.label(text="Id: %d" % (item.text_id)) # avoids renaming the item by accident @@ -339,7 +309,6 @@ class ABC3D_UL_texts(bpy.types.UIList): def invoke(self, context, event): pass - class ABC3D_PT_Panel(bpy.types.Panel): bl_label = f"{__name__} panel" bl_category = "ABC3D" @@ -349,20 +318,16 @@ class ABC3D_PT_Panel(bpy.types.Panel): def draw(self, context): layout = self.layout - icon = "NONE" + icon = 'NONE' if len(context.scene.abc3d_data.available_fonts) == 0: - icon = "ERROR" - layout.row().label(text="no fonts loaded yet") + icon = 'ERROR' + layout.row().label(text='no fonts loaded yet') - layout.operator(f"{__name__}.install_font", text="Install new font") - layout.operator( - f"{__name__}.load_installed_fonts", text="load installed fonts", icon=icon - ) - layout.operator( - f"{__name__}.open_asset_directory", - text="open asset directory", - icon="FILEBROWSER", - ) + layout.operator(f"{__name__}.install_font", text='Install new font') + layout.operator(f"{__name__}.load_installed_fonts", + text="load installed fonts", icon=icon) + layout.operator(f"{__name__}.open_asset_directory", + text="open asset directory", icon='FILEBROWSER') class ABC3D_PT_FontList(bpy.types.Panel): @@ -380,61 +345,44 @@ class ABC3D_PT_FontList(bpy.types.Panel): abc3d_data = scene.abc3d_data layout.label(text="Available Fonts") - layout.template_list( - "ABC3D_UL_fonts", - "", - abc3d_data, - "available_fonts", - abc3d_data, - "active_font_index", - ) + layout.template_list("ABC3D_UL_fonts", "", abc3d_data, + "available_fonts", abc3d_data, "active_font_index") if abc3d_data.active_font_index >= 0: available_font = abc3d_data.available_fonts[abc3d_data.active_font_index] font_name = available_font.font_name face_name = available_font.face_name available_glyphs = sorted( - Font.fonts[font_name].faces[face_name].glyphs_in_fontfile - ) - loaded_glyphs = sorted(Font.fonts[font_name].faces[face_name].loaded_glyphs) + Font.fonts[font_name].faces[face_name].glyphs_in_fontfile) + loaded_glyphs = sorted( + Font.fonts[font_name].faces[face_name].loaded_glyphs) box = layout.box() box.row().label(text=f"Font Name: {font_name}") box.row().label(text=f"Face Name: {face_name}") n = 16 n_rows = int(len(available_glyphs) / n) - box.row().label(text="Glyphs:") + box.row().label(text=f"Glyphs:") subbox = box.box() for i in range(0, n_rows + 1): - text = "".join( - [ - f"{u}" - for ui, u in enumerate(available_glyphs) - if ui < (i + 1) * n and ui >= i * n - ] - ) + text = ''.join([f"{u}" for ui, u in enumerate( + available_glyphs) if ui < (i+1) * n and ui >= i * n]) scale_y = 0.5 row = subbox.row() row.scale_y = scale_y - row.alignment = "CENTER" + row.alignment = 'CENTER' row.label(text=text) n_rows = int(len(loaded_glyphs) / n) - box.row().label(text="Loaded/Used Glyphs:") + box.row().label(text=f"Loaded/Used Glyphs:") subbox = box.box() for i in range(0, n_rows + 1): - text = "".join( - [ - f"{u}" - for ui, u in enumerate(loaded_glyphs) - if ui < (i + 1) * n and ui >= i * n - ] - ) + text = ''.join([f"{u}" for ui, u in enumerate( + loaded_glyphs) if ui < (i+1) * n and ui >= i * n]) scale_y = 0.5 row = subbox.row() row.scale_y = scale_y row.label(text=text) row = layout.row() - oper_lf = row.operator( - f"{__name__}.load_font", text="Load all glyphs in memory" - ) + oper_lf = row.operator(f"{__name__}.load_font", + text='Load all glyphs in memory') oper_lf.font_name = font_name oper_lf.face_name = face_name @@ -450,10 +398,7 @@ class ABC3D_PT_TextPlacement(bpy.types.Panel): @classmethod def poll(self, context): - if ( - type(context.active_object) != type(None) - and context.active_object.type == "CURVE" - ): + if type(context.active_object) != type(None) and context.active_object.type == 'CURVE': self.can_place = True else: self.can_place = False @@ -468,7 +413,7 @@ class ABC3D_PT_TextPlacement(bpy.types.Panel): placerow = layout.row() placerow.enabled = self.can_place - placerow.operator(f"{__name__}.placetext", text="Place Text") + placerow.operator(f"{__name__}.placetext", text='Place Text') if not self.can_place: layout.label(text="Cannot place Text.") layout.label(text="Select a curve as active object.") @@ -500,43 +445,24 @@ class ABC3D_PT_TextManagement(bpy.types.Panel): continue remove_me = True for c in t.text_object.children: - if ( - len(c.users_collection) > 0 - and not isinstance(c.get(f"{utils.prefix()}_linked_textobject"), None) - and c.get(f"{utils.prefix()}_linked_textobject") == t.text_id - ): + if len(c.users_collection) > 0 and (c.get(f"{utils.prefix()}_linked_textobject")) != type(None) and c.get(f"{utils.prefix()}_linked_textobject") == t.text_id: remove_me = False # not sure how to solve this reliably atm, # we need to reassign the glyph, but also get the proper properties from glyph_properties # these might be there in t.glyphs, but linked to removed objects # or they might be lost - if type( - next( - ( - g - for g in t.glyphs - if type(g.glyph_object) == type(None) - ), - None, - ) - ) == type(None): - g = next( - ( - g - for g in t.glyphs - if type(g.glyph_object) == type(None) - ), - None, - ) + if type(next((g for g in t.glyphs if type(g.glyph_object) == type(None)), None)) == type(None): + g = next((g for g in t.glyphs if type( + g.glyph_object) == type(None)), None) # for g in t.glyphs: - # if type(g) == type(None): - # print("IS NONE") - # if type(g.glyph_object) == type(None): - # print("go IS NONE") - # else: - # if g.glyph_object == c: - # # print(g.glyph_object.name) - # pass + # if type(g) == type(None): + # print("IS NONE") + # if type(g.glyph_object) == type(None): + # print("go IS NONE") + # else: + # if g.glyph_object == c: + # # print(g.glyph_object.name) + # pass if remove_me: remove_list.append(i) @@ -548,7 +474,6 @@ class ABC3D_PT_TextManagement(bpy.types.Panel): def delif(o, p): if p in o: del o[p] - delif(mom, f"{utils.prefix()}_linked_textobject") delif(mom, f"{utils.prefix()}_font_name") delif(mom, f"{utils.prefix()}_face_name") @@ -562,10 +487,8 @@ class ABC3D_PT_TextManagement(bpy.types.Panel): for i, t in enumerate(abc3d_data.available_texts): if context.active_object == t.text_object: active_text_index = i - if ( - hasattr(context.active_object, "parent") - and context.active_object.parent == t.text_object - ): + if (hasattr(context.active_object, "parent") and + context.active_object.parent == t.text_object): active_text_index = i if active_text_index != abc3d_data.active_text_index: @@ -583,106 +506,12 @@ class ABC3D_PT_TextManagement(bpy.types.Panel): abc3d_data = scene.abc3d_data layout.label(text="Text Objects") - layout.template_list( - "ABC3D_UL_texts", - "", - abc3d_data, - "available_texts", - abc3d_data, - "active_text_index", - ) - layout.row().operator(f"{__name__}.remove_text", text="Remove Textobject") + layout.template_list("ABC3D_UL_texts", "", abc3d_data, + "available_texts", abc3d_data, "active_text_index") + layout.row().operator( + f"{__name__}.remove_text", text="Remove Textobject") -class ABC3D_PG_FontCreation(bpy.types.PropertyGroup): - bl_label = "Font Creation Properties" - # bl_parent_id = "ABC3D_PG_NamingHelper" - # bl_category = "ABC3D" - # bl_space_type = "VIEW_3D" - # bl_region_type = "UI" - # bl_options = {"DEFAULT_CLOSED"} - - def naming_glyph_id_update_callback(self, context): - glyph_name = Font.glyph_to_name(self.naming_glyph_id) - if self.naming_glyph_full: - self.naming_glyph_name = f"{glyph_name}_{self.font_name}_{self.face_name}" - else: - self.naming_glyph_name = glyph_name - - naming_glyph_id: bpy.props.StringProperty( - name="", - description="find proper naming for a glyph", - default="", - maxlen=32, - update=naming_glyph_id_update_callback, - ) - - naming_glyph_name: bpy.props.StringProperty( - name="", - description="find proper naming for a glyph", - default="", - maxlen=1024, - ) - - naming_glyph_full: bpy.props.BoolProperty( - default=True, - description="Generate full name", - update=naming_glyph_id_update_callback, - ) - - font_name: bpy.props.StringProperty( - name="", - description="Font name", - default="NM_Origin", - update=naming_glyph_id_update_callback, - ) - - face_name: bpy.props.StringProperty( - name="", - description="FontFace name", - default="Tender", - update=naming_glyph_id_update_callback, - ) - -class ABC3D_OT_NamingHelper(bpy.types.Operator): - bl_label = "Font Creation Naming Helper Apply To Active Object" - bl_idname = f"{__name__}.apply_naming_helper" - bl_options = {"REGISTER", "UNDO"} - - def execute(self, context): - abc3d_font_creation = context.scene.abc3d_font_creation - name = abc3d_font_creation.naming_glyph_name - context.active_object.name = name - return {"FINISHED"} - - -class ABC3D_PT_NamingHelper(bpy.types.Panel): - bl_label = "Naming Helper" - bl_parent_id = "ABC3D_PT_FontCreation" - bl_category = "ABC3D" - bl_space_type = "VIEW_3D" - bl_region_type = "UI" - bl_options = {"DEFAULT_CLOSED"} - - def draw(self, context): - layout = self.layout - scene = context.scene - - abc3d_font_creation = scene.abc3d_font_creation - - box = layout.box() - box.label(text="Glyph Naming Helper") - box.row().prop(abc3d_font_creation, "naming_glyph_full") - box.label(text="Glyph Character Input") - box.row().prop(abc3d_font_creation, "naming_glyph_id") - box.label(text="Font name:") - box.row().prop(abc3d_font_creation, "font_name") - box.label(text="FontFace name:") - box.row().prop(abc3d_font_creation, "face_name") - box.label(text="Glyph Output Name") - box.row().prop(abc3d_font_creation, "naming_glyph_name") - box.row().operator(f"{__name__}.apply_naming_helper", text="Apply name to active object") - class ABC3D_PT_FontCreation(bpy.types.Panel): bl_label = "Font Creation" bl_parent_id = "ABC3D_PT_Panel" @@ -694,35 +523,32 @@ class ABC3D_PT_FontCreation(bpy.types.Panel): def draw(self, context): layout = self.layout wm = context.window_manager + scene = context.scene + + abc3d_data = scene.abc3d_data layout.row().operator( - f"{__name__}.toggle_abc3d_collection", text="Toggle Collection" - ) - layout.row().operator( - f"{__name__}.create_font_from_objects", text="Create/Extend Font" - ) - layout.row().operator( - f"{__name__}.save_font_to_file", text="Export Font To File" - ) + f"{__name__}.create_font_from_objects", text='Create/Extend Font') + box = layout.box() + box.row().label(text="Exporting a fontfile") + box.row().label(text="1. Select export directory:") + box.prop(abc3d_data, 'export_dir') + box.row().label(text="2. More options and export:") + box.row().operator(f"{__name__}.save_font_to_file", + text='Export Font To File') + layout.row().operator( + f"{__name__}.toggle_abc3d_collection", text='Toggle Collection') box = layout.box() box.label(text="metrics") box.row().operator( - f"{__name__}.add_default_metrics", text="Add Default Metrics" - ) - box.row().operator(f"{__name__}.remove_metrics", text="Remove Metrics") - box.row().operator(f"{__name__}.align_metrics", text="Align Metrics") + f"{__name__}.add_default_metrics", text='Add Default Metrics') + box.row().operator(f"{__name__}.remove_metrics", text='Remove Metrics') + box.row().operator(f"{__name__}.align_metrics", text='Align Metrics') box.row().operator( - f"{__name__}.align_metrics_to_active_object", - text="Align Metrics to Active Object", - ) + f"{__name__}.align_metrics_to_active_object", text='Align Metrics to Active Object') layout.row().operator( - f"{__name__}.temporaryhelper", text="Debug Function Do Not Use" - ) - box.label(text="origin points") - box.row().operator(f"{__name__}.align_origins_to_active_object", text="Align origins to Active Object") - # box.row().operator(f"{__name__}.align_origins_to_metrics", text="Align origins to Metrics (left)") - # box.row().operator(f"{__name__}.fix_objects_metrics_origins", text="Fix objects metrics origins") + f"{__name__}.temporaryhelper", text='Debug Function Do Not Use') class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): @@ -734,68 +560,44 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): def get_active_text_properties(self): # and bpy.context.object.select_get(): - a_o = bpy.context.active_object - if a_o is not None: - if f"{utils.prefix()}_linked_textobject" in a_o: - text_index = a_o[f"{utils.prefix()}_linked_textobject"] - return bpy.context.scene.abc3d_data.available_texts[text_index] - elif a_o.parent is not None and f"{utils.prefix()}_linked_textobject" in a_o.parent: - text_index = a_o.parent[f"{utils.prefix()}_linked_textobject"] - return bpy.context.scene.abc3d_data.available_texts[text_index] - else: - for t in bpy.context.scene.abc3d_data.available_texts: - if butils.is_or_has_parent(bpy.context.active_object, t.text_object, max_depth=4): - return t + if type(bpy.context.active_object) != type(None): + for t in bpy.context.scene.abc3d_data.available_texts: + if bpy.context.active_object == t.text_object: + return t + if bpy.context.active_object.parent == t.text_object: + return t return None - # NOTE: HERE - def get_active_glyph_properties(self): - a_o = bpy.context.active_object - if a_o is not None: - if (f"{utils.prefix()}_linked_textobject" in a_o - and f"{utils.prefix()}_glyph_index" in a_o): - text_index = a_o[f"{utils.prefix()}_linked_textobject"] - glyph_index = a_o[f"{utils.prefix()}_glyph_index"] - return bpy.context.scene.abc3d_data.available_texts[text_index].glyphs[glyph_index] - else: - for t in bpy.context.scene.abc3d_data.available_texts: - if butils.is_or_has_parent(a_o, t.text_object, if_is_parent=False, max_depth=4): - for g in t.glyphs: - if butils.is_or_has_parent(a_o, g.glyph_object, max_depth=4): - return g - return None - - # def font_items_callback(self, context): - # items = [] - # fonts = Font.get_loaded_fonts_and_faces() - # for f in fonts: - # items.append((f"{f[0]} {f[1]}", f"{f[0]} {f[1]}", "")) - # return items + # items = [] + # fonts = Font.get_loaded_fonts_and_faces() + # for f in fonts: + # items.append((f"{f[0]} {f[1]}", f"{f[0]} {f[1]}", "")) + # return items # def font_default_callback(self, context): - # t = self.get_active_text_properties(self) - # if type(t) != type(None): - # return f"{t.font_name} {t.face_name}" - # else: - # return None + # t = self.get_active_text_properties(self) + # if type(t) != type(None): + # return f"{t.font_name} {t.face_name}" + # else: + # return None # def font_update_callback(self, context): - # font_name, face_name = self.font.split(" ") - # t = self.get_active_text_properties(self) - # t.font_name = font_name - # t.face_name = face_name - # butils.set_text_on_curve(t) + # font_name, face_name = self.font.split(" ") + # t = self.get_active_text_properties(self) + # t.font_name = font_name + # t.face_name = face_name + # butils.set_text_on_curve(t) # font: bpy.props.EnumProperty( - # items=font_items_callback, - # default=font_default_callback, - # update=font_update_callback, - # ) + # items=font_items_callback, + # default=font_default_callback, + # update=font_update_callback, + # ) @classmethod def poll(self, context): - return self.get_active_text_properties(self) is not None + return type(self.get_active_text_properties(self)) != type(None) def draw(self, context): layout = self.layout @@ -804,16 +606,11 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): abc3d_data = scene.abc3d_data props = self.get_active_text_properties() - glyph_props = self.get_active_glyph_properties() - if props is None or props.text_object is None: + if type(props) == type(None) or type(props.text_object) == type(None): # this should not happen # as then polling does not work # however, we are paranoid - if props is None: - layout.label(text="props is none") - elif props.text_object is None: - layout.label(text="props.text_object is none") return layout.label(text=f"Mom: {props.text_object.name}") @@ -824,33 +621,22 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): layout.row().prop(props, "offset") layout.row().prop(props, "compensate_curvature") layout.row().prop(props, "ignore_orientation") - layout.row().prop(props, "loop_in") - layout.row().prop(props, "loop_out") layout.column().prop(props, "translation") layout.column().prop(props, "orientation") - if glyph_props is None: - return - box = layout.box() - box.label(text=f"{glyph_props.glyph_id}") - box.row().prop(glyph_props, "letter_spacing") - - class ABC3D_OT_InstallFont(bpy.types.Operator): """Install or load Fontfile from path above. - (Format must be *.glb or *.gltf)""" - +(Format must be *.glb or *.gltf)""" bl_idname = f"{__name__}.install_font" bl_label = "Load Font" - bl_options = {"REGISTER", "UNDO"} + bl_options = {'REGISTER', 'UNDO'} def font_path_update_callback(self, context): - font_path = butils.bpy_to_abspath(self.font_path) - if os.path.exists(font_path): - print(f"font_path_update: {font_path} does exist") + if os.path.exists(self.font_path): + print(f"{self.font_path} does exist") else: - print(f"font_path_update: {font_path} does not exist") + print(f"{self.font_path} does not exist") font_path: bpy.props.StringProperty( name="Font path", @@ -858,8 +644,7 @@ class ABC3D_OT_InstallFont(bpy.types.Operator): default="", maxlen=1024, update=font_path_update_callback, - subtype="FILE_PATH", - ) + subtype="FILE_PATH") install_in_assets: bpy.props.BoolProperty( name="install in assets", @@ -867,11 +652,9 @@ class ABC3D_OT_InstallFont(bpy.types.Operator): default=True, ) - load_into_memory: bpy.props.BoolProperty( - name="load font data into memory", - description="if false, it will load font data on demand", - default=False, - ) + load_into_memory: bpy.props.BoolProperty(name="load font data into memory", + description="if false, it will load font data on demand", + default=False) def draw(self, context): abc3d_data = context.scene.abc3d_data @@ -881,7 +664,8 @@ class ABC3D_OT_InstallFont(bpy.types.Operator): layout.row().prop(self, "install_in_assets") if not self.install_in_assets and not self.load_into_memory: layout.label(text="If the fontfile is not installed,") - layout.label(text="and the font is not loaded in memory completely,") + layout.label( + text="and the font is not loaded in memory completely,") layout.label(text="the fontfile should not be moved.") layout.row().prop(self, "load_into_memory") if self.load_into_memory: @@ -893,14 +677,14 @@ class ABC3D_OT_InstallFont(bpy.types.Operator): def invoke(self, context, event): # self.font_path = butils.bpy_to_abspath(self.font_path) # if not os.path.exists(self.font_path): - # bpy.app.timers.register(lambda: butils.ShowMessageBox( - # title=f"{__name__} Warning", - # icon="ERROR", - # message=[ - # f"We believe the font path ({self.font_path}) does not exist.", - # f"Did you select your fontfile in the field above the 'Install new font'-button?", - # ], - # ), first_interval=0.1) + # bpy.app.timers.register(lambda: butils.ShowMessageBox( + # title=f"{__name__} Warning", + # icon="ERROR", + # message=[ + # f"We believe the font path ({self.font_path}) does not exist.", + # f"Did you select your fontfile in the field above the 'Install new font'-button?", + # ], + # ), first_interval=0.1) return context.window_manager.invoke_props_dialog(self) def execute(self, context): @@ -913,19 +697,18 @@ class ABC3D_OT_InstallFont(bpy.types.Operator): title=f"{__name__} Warning", icon="ERROR", message=[ - "Could not install font.", + f"Could not install font.", f"We believe the font path ({font_path}) does not exist.", - "If this is an error, please let us know.", + f"If this is an error, please let us know.", ], ) - return {"CANCELLED"} + return {'CANCELLED'} if self.install_in_assets: preferences = getPreferences(context) filename = os.path.basename(font_path) target = os.path.join(preferences.assets_dir, "fonts", filename) import shutil - os.makedirs(os.path.dirname(target), exist_ok=True) shutil.copyfile(font_path, target) # def register_load(target, load=False): @@ -940,47 +723,40 @@ class ABC3D_OT_InstallFont(bpy.types.Operator): if self.load_into_memory: butils.load_font_from_filepath(font_path) - return {"FINISHED"} + return {'FINISHED'} class ABC3D_OT_OpenAssetDirectory(bpy.types.Operator): """Open Asset Directory""" - bl_idname = f"{__name__}.open_asset_directory" bl_label = "Opens asset directory." - bl_options = {"REGISTER", "UNDO"} + bl_options = {'REGISTER', 'UNDO'} def execute(self, context): preferences = getPreferences(context) directory = os.path.realpath(preferences.assets_dir) if os.path.exists(directory): utils.open_file_browser(directory) - return {"FINISHED"} + return {'FINISHED'} else: butils.ShowMessageBox( title=f"{__name__} Warning", icon="ERROR", - message=( - "Asset directory does not exist.", - f"Command failed trying to access '{directory}'.", - "Please, make sure it exists or chose another directory.", - ), - ) - return {"CANCELLED"} + message=("Asset directory does not exist.", + f"Command failed trying to access '{directory}'.", + "Please, make sure it exists or chose another directory.")) + return {'CANCELLED'} class ABC3D_OT_LoadInstalledFonts(bpy.types.Operator): """Load installed fontfiles from datapath.""" - bl_idname = f"{__name__}.load_installed_fonts" bl_label = "Loading installed Fonts." - bl_options = {"REGISTER", "UNDO"} + bl_options = {'REGISTER', 'UNDO'} - load_into_memory: bpy.props.BoolProperty( - name="load font data into memory", - description="if false, it will load font data on demand", - default=False, - ) + load_into_memory: bpy.props.BoolProperty(name="load font data into memory", + description="if false, it will load font data on demand", + default=False) def draw(self, context): layout = self.layout @@ -1002,19 +778,22 @@ class ABC3D_OT_LoadInstalledFonts(bpy.types.Operator): butils.load_installed_fonts() else: butils.register_installed_fonts() - butils.ShowMessageBox("Loading Fonts", "INFO", "Updating Data Structures.") + butils.ShowMessageBox("Loading Fonts", + 'INFO', + "Updating Data Structures.") butils.update_available_fonts() - butils.ShowMessageBox("Loading Fonts", "INFO", "Done loading installed fonts.") + butils.ShowMessageBox("Loading Fonts", + 'INFO', + "Done loading installed fonts.") - return {"FINISHED"} + return {'FINISHED'} class ABC3D_OT_LoadFont(bpy.types.Operator): """Load all glyphs from a specific font in memory.\nThis can take a while and slow down Blender.""" - bl_idname = f"{__name__}.load_font" bl_label = "Loading Font." - bl_options = {"REGISTER", "UNDO"} + bl_options = {'REGISTER', 'UNDO'} font_name: bpy.props.StringProperty() face_name: bpy.props.StringProperty() @@ -1023,138 +802,62 @@ class ABC3D_OT_LoadFont(bpy.types.Operator): filepaths = Font.fonts[self.font_name].faces[self.face_name].filepaths for f in filepaths: butils.load_font_from_filepath(f) - return {"FINISHED"} + return {'FINISHED'} class ABC3D_OT_AddDefaultMetrics(bpy.types.Operator): """Add default metrics to selected objects""" - bl_idname = f"{__name__}.add_default_metrics" bl_label = "Add default metrics" - bl_options = {"REGISTER", "UNDO"} + bl_options = {'REGISTER', 'UNDO'} def execute(self, context): objects = bpy.context.selected_objects butils.add_default_metrics_to_objects(objects) - return {"FINISHED"} + return {'FINISHED'} class ABC3D_OT_RemoveMetrics(bpy.types.Operator): """Remove metrics from selected objects""" - bl_idname = f"{__name__}.remove_metrics" bl_label = "Remove metrics" - bl_options = {"REGISTER", "UNDO"} + bl_options = {'REGISTER', 'UNDO'} def execute(self, context): objects = bpy.context.selected_objects butils.remove_metrics_from_objects(objects) - return {"FINISHED"} + return {'FINISHED'} class ABC3D_OT_AlignMetricsToActiveObject(bpy.types.Operator): - """Align metrics of selected objects to metrics of active object. - - The metrics of the active object are not changed and is taken as a reference for all other objects. - """ - + """Align metrics of selected objects to metrics of active object""" bl_idname = f"{__name__}.align_metrics_to_active_object" bl_label = "Align metrics to active object" - bl_options = {"REGISTER", "UNDO"} + bl_options = {'REGISTER', 'UNDO'} def execute(self, context): objects = bpy.context.selected_objects butils.align_metrics_of_objects_to_active_object(objects) - return {"FINISHED"} + return {'FINISHED'} class ABC3D_OT_AlignMetrics(bpy.types.Operator): - """Align metrics of selected objects to each other. - - The metrics of all objects are merged and expanded to fit for all objects.""" - + """Align metrics of selected objects to each other""" bl_idname = f"{__name__}.align_metrics" bl_label = "Align metrics" - bl_options = {"REGISTER", "UNDO"} + bl_options = {'REGISTER', 'UNDO'} def execute(self, context): objects = bpy.context.selected_objects butils.align_metrics_of_objects(objects) - return {"FINISHED"} + return {'FINISHED'} -class ABC3D_OT_AlignOriginsToActiveObject(bpy.types.Operator): - """Align origins of selected objects to origin of active object on one axis.""" - - bl_idname = f"{__name__}.align_origins_to_active_object" - bl_label = "Align origins to Active Object" - bl_options = {"REGISTER", "UNDO"} - - enum_axis = (('0','X',''),('1','Y',''),('2','Z','')) - axis: bpy.props.EnumProperty(items = enum_axis, default='2') - - def execute(self, context): - objects = bpy.context.selected_objects - butils.align_origins_to_active_object(objects, int(self.axis)) - return {"FINISHED"} - -# class ABC3D_OT_AlignOriginsToMetrics(bpy.types.Operator): - # """Align origins of selected objects to their metrics left border. - - # Be aware that shifting the origin will also shift the pivot point around which an object rotates. - - # If an object does not have metrics, it will be ignored.""" - - # bl_idname = f"{__name__}.align_origins_to_metrics" - # bl_label = "Align origins to metrics metrics" - # bl_options = {"REGISTER", "UNDO"} - - # ignore_warning: bpy.props.BoolProperty( - # name="Do not warn in the future", - # description="Do not warn in the future", - # default=False, - # ) - - # def draw(self, context): - # layout = self.layout - # layout.row().label(text="Warning!") - # layout.row().label(text="This also shifts the pivot point around which the glyph rotates.") - # layout.row().label(text="This may not be what you want.") - # layout.row().label(text="Glyph advance derives from metrics boundaries, not origin points.") - # layout.row().label(text="If you are sure about what you're doing, please continue.") - # layout.row().prop(self, "ignore_warning") - - # def invoke(self, context, event): - # if not self.ignore_warning: - # wm = context.window_manager - # return wm.invoke_props_dialog(self) - # return self.execute(context) - - # def execute(self, context): - # objects = bpy.context.selected_objects - # butils.align_origins_to_metrics(objects) - # butils.fix_objects_metrics_origins(objects) - # return {"FINISHED"} - -# class ABC3D_OT_FixObjectsMetricsOrigins(bpy.types.Operator): - # """Align metrics origins of selected objects to their metrics bounding box. - - # If an object does not have metrics, it will be ignored.""" - - # bl_idname = f"{__name__}.fix_objects_metrics_origins" - # bl_label = "Fix metrics origin of all selected objects" - # bl_options = {"REGISTER", "UNDO"} - - # def execute(self, context): - # objects = bpy.context.selected_objects - # butils.fix_objects_metrics_origins(objects) - # return {"FINISHED"} class ABC3D_OT_TemporaryHelper(bpy.types.Operator): """Temporary Helper ABC3D\nThis could do anything.\nIt's just there to make random functions available for testing.""" - bl_idname = f"{__name__}.temporaryhelper" bl_label = "Temp Font" - bl_options = {"REGISTER", "UNDO"} + bl_options = {'REGISTER', 'UNDO'} def execute(self, context): global shared @@ -1175,21 +878,19 @@ class ABC3D_OT_TemporaryHelper(bpy.types.Operator): # butils.add_metrics_obj_from_bound_box(o, metrics) # bpy.app.timers.register(lambda: butils.remove_metrics_from_objects(objects), first_interval=5) - return {"FINISHED"} + return {'FINISHED'} class ABC3D_OT_RemoveText(bpy.types.Operator): """Remove Text 3D""" - bl_idname = f"{__name__}.remove_text" bl_label = "Remove Text" - bl_options = {"REGISTER", "UNDO"} + bl_options = {'REGISTER', 'UNDO'} remove_objects: bpy.props.BoolProperty( name="Remove Objects", description="Remove both ABC3D text functionality and the objects/meshes", - default=True, - ) + default=True) def invoke(self, context, event): wm = context.window_manager @@ -1201,9 +902,8 @@ class ABC3D_OT_RemoveText(bpy.types.Operator): butils.ShowMessageBox( title="No text selected", message=("Please select a text."), - icon="GHOST_ENABLED", - ) - return {"CANCELLED"} + icon='GHOST_ENABLED') + return {'CANCELLED'} i = abc3d_data.active_text_index if type(abc3d_data.available_texts[i].text_object) != type(None): @@ -1212,7 +912,6 @@ class ABC3D_OT_RemoveText(bpy.types.Operator): def delif(o, p): if p in o: del o[p] - delif(mom, f"{utils.prefix()}_type") delif(mom, f"{utils.prefix()}_linked_textobject") delif(mom, f"{utils.prefix()}_font_name") @@ -1231,15 +930,14 @@ class ABC3D_OT_RemoveText(bpy.types.Operator): abc3d_data.available_texts.remove(i) - return {"FINISHED"} + return {'FINISHED'} class ABC3D_OT_PlaceText(bpy.types.Operator): """Place Text 3D on active object""" - bl_idname = f"{__name__}.placetext" bl_label = "Place Text" - bl_options = {"REGISTER", "UNDO"} + bl_options = {'REGISTER', 'UNDO'} def font_items_callback(self, context): items = [] @@ -1253,9 +951,15 @@ class ABC3D_OT_PlaceText(bpy.types.Operator): self.font_name = font_name self.face_name = face_name - font_name: bpy.props.StringProperty(options={"HIDDEN"}) - face_name: bpy.props.StringProperty(options={"HIDDEN"}) - font: bpy.props.EnumProperty(items=font_items_callback, update=font_update_callback) + font_name: bpy.props.StringProperty( + options={'HIDDEN'} + ) + face_name: bpy.props.StringProperty( + options={'HIDDEN'} + ) + font: bpy.props.EnumProperty(items=font_items_callback, + update=font_update_callback + ) text: bpy.props.StringProperty( name="Text", description="The text.", @@ -1275,22 +979,22 @@ class ABC3D_OT_PlaceText(bpy.types.Operator): font_size: bpy.props.FloatProperty( name="Font Size", default=1.0, - subtype="NONE", + subtype='NONE', ) offset: bpy.props.FloatProperty( name="Offset", default=0.0, - subtype="NONE", + subtype='NONE', ) translation: bpy.props.FloatVectorProperty( name="Translation", default=(0.0, 0.0, 0.0), - subtype="TRANSLATION", + subtype='TRANSLATION', ) orientation: bpy.props.FloatVectorProperty( name="Orientation", default=(1.5707963267948966, 0.0, 0.0), # 90 degrees in radians - subtype="EULER", + subtype='EULER', ) def invoke(self, context, event): @@ -1311,7 +1015,7 @@ class ABC3D_OT_PlaceText(bpy.types.Operator): # font_name = font.font_name # face_name = font.face_name - distribution_type = "DEFAULT" + distribution_type = 'DEFAULT' text_id = 0 for i, tt in enumerate(abc3d_data.available_texts): @@ -1320,18 +1024,18 @@ class ABC3D_OT_PlaceText(bpy.types.Operator): t = abc3d_data.available_texts.add() # If you wish to set a value and not fire an update, set the id property. # A property defined via bpy.props for example ob.prop is stored as ob["prop"] once set to non default. - t["text_id"] = text_id + t['text_id'] = text_id # t['font'] = self.font # enums want to be set as attribute - t["font_name"] = self.font_name - t["face_name"] = self.face_name - t["text_object"] = selected - t.text = self.text - t["letter_spacing"] = self.letter_spacing - t["font_size"] = self.font_size - t["offset"] = self.offset - t["translation"] = self.translation - t["orientation"] = self.orientation - t["distribution_type"] = distribution_type + t['font_name'] = self.font_name + t['face_name'] = self.face_name + t.text_object = selected + t['text'] = self.text + t['letter_spacing'] = self.letter_spacing + t['font_size'] = self.font_size + t['offset'] = self.offset + t['translation'] = self.translation + t['orientation'] = self.orientation + t['distribution_type'] = distribution_type t.font = self.font # enums want to be set as attribute # this also calls the update function # so we don't need to prepare/set again @@ -1347,22 +1051,17 @@ class ABC3D_OT_PlaceText(bpy.types.Operator): message=( "Please select an object.", "It will be used to put the type on.", - "Thank you :)", - ), - icon="GHOST_ENABLED", - ) + "Thank you :)"), + icon='GHOST_ENABLED') - return {"FINISHED"} + return {'FINISHED'} class ABC3D_OT_ToggleABC3DCollection(bpy.types.Operator): - """Toggle ABC3D Collection. - - This will show the Fonts and Glyphs currently loaded by ABC3D. Useful for font creation, debugging and inspection.""" - + """Toggle ABC3D Collection""" bl_idname = f"{__name__}.toggle_abc3d_collection" bl_label = "Toggle Collection visibility" - bl_options = {"REGISTER", "UNDO"} + bl_options = {'REGISTER', 'UNDO'} def execute(self, context): scene = context.scene @@ -1370,68 +1069,56 @@ class ABC3D_OT_ToggleABC3DCollection(bpy.types.Operator): if fontcollection is None: self.report( - {"INFO"}, - f"{bl_info['name']}: There is no collection. Did you use or create any glyphs yet?", - ) + {'INFO'}, f"{bl_info['name']}: There is no collection. Did you use or create any glyphs yet?") elif scene.collection.children.find(fontcollection.name) < 0: scene.collection.children.link(fontcollection) - self.report({"INFO"}, f"{bl_info['name']}: show collection") + self.report({'INFO'}, f"{bl_info['name']}: show collection") else: scene.collection.children.unlink(fontcollection) - self.report({"INFO"}, f"{bl_info['name']}: hide collection") + self.report({'INFO'}, f"{bl_info['name']}: hide collection") - return {"FINISHED"} + return {'FINISHED'} class ABC3D_OT_SaveFontToFile(bpy.types.Operator): """Save font to file""" - bl_idname = f"{__name__}.save_font_to_file" bl_label = "Save Font" - bl_options = {"REGISTER", "UNDO"} + bl_options = {'REGISTER', 'UNDO'} def invoke(self, context, event): wm = context.window_manager preferences = getPreferences(context) abc3d_data = context.scene.abc3d_data if abc3d_data.export_dir == "": - abc3d_data.export_dir = os.path.join(preferences.assets_dir, "fonts") + abc3d_data.export_dir = os.path.join( + preferences.assets_dir, "fonts") return wm.invoke_props_dialog(self) def draw(self, context): abc3d_data = context.scene.abc3d_data layout = self.layout layout.label(text="Available Fonts") - layout.template_list( - "ABC3D_UL_fonts", - "", - abc3d_data, - "available_fonts", - abc3d_data, - "active_font_index", - ) + layout.template_list("ABC3D_UL_fonts", "", abc3d_data, + "available_fonts", abc3d_data, "active_font_index") available_font = abc3d_data.available_fonts[abc3d_data.active_font_index] font_name = available_font.font_name face_name = available_font.face_name - loaded_glyphs = sorted(Font.fonts[font_name].faces[face_name].loaded_glyphs) + loaded_glyphs = sorted( + Font.fonts[font_name].faces[face_name].loaded_glyphs) n = 16 n_rows = int(len(loaded_glyphs) / n) box = layout.box() - box.row().label(text="Glyphs to be exported:") + box.row().label(text=f"Glyphs to be exported:") subbox = box.box() for i in range(0, n_rows + 1): - text = "".join( - [ - f"{u}" - for ui, u in enumerate(loaded_glyphs) - if ui < (i + 1) * n and ui >= i * n - ] - ) + text = ''.join([f"{u}" for ui, u in enumerate( + loaded_glyphs) if ui < (i+1) * n and ui >= i * n]) scale_y = 0.5 row = subbox.row() row.scale_y = scale_y row.label(text=text) - layout.prop(abc3d_data, "export_dir") + layout.prop(abc3d_data, 'export_dir') def execute(self, context): global shared @@ -1442,21 +1129,22 @@ class ABC3D_OT_SaveFontToFile(bpy.types.Operator): # check if all is good to proceed if fontcollection is None: - self.report({"INFO"}, f"{bl_info['name']}: There is no collection") - return {"CANCELLED"} + self.report({'INFO'}, f"{bl_info['name']}: There is no collection") + return {'CANCELLED'} if abc3d_data.active_font_index < 0: - self.report({"INFO"}, f"{bl_info['name']}: There is no active font") - return {"CANCELLED"} + self.report( + {'INFO'}, f"{bl_info['name']}: There is no active font") + return {'CANCELLED'} if len(abc3d_data.available_fonts) <= abc3d_data.active_font_index: - self.report({"INFO"}, f"{bl_info['name']}: Active font is not available") - return {"CANCELLED"} + self.report( + {'INFO'}, f"{bl_info['name']}: Active font is not available") + return {'CANCELLED'} # save state to restore later - was_fontcollection_linked = ( - scene.collection.children.find(fontcollection.name) >= 0 - ) + was_fontcollection_linked = scene.collection.children.find( + fontcollection.name) >= 0 was_selection = [] for obj in bpy.context.selected_objects: was_selection.append(obj) @@ -1469,13 +1157,11 @@ class ABC3D_OT_SaveFontToFile(bpy.types.Operator): # print(selected_font.font_name) self.report( - {"INFO"}, - f"{bl_info['name']}: {selected_font.font_name} {selected_font.face_name}", - ) + {'INFO'}, f"{bl_info['name']}: {selected_font.font_name} {selected_font.face_name}") preferences = getPreferences(context) print(f"assets folder: {preferences.assets_dir}") - bpy.ops.scene.new(type="FULL_COPY") + bpy.ops.scene.new(type='FULL_COPY') linked_collections = bpy.context.scene.collection.children.values() for c in linked_collections: @@ -1511,12 +1197,13 @@ class ABC3D_OT_SaveFontToFile(bpy.types.Operator): filepath=filepath, check_existing=False, # GLB or GLTF_SEPARATE (also change filepath) - export_format="GLB", + export_format='GLB', export_extras=True, use_selection=True, use_active_scene=True, ) - bpy.app.timers.register(lambda: bpy.ops.scene.delete(), first_interval=1) + bpy.app.timers.register( + lambda: bpy.ops.scene.delete(), first_interval=1) # bpy.ops.scene.delete() # restore() @@ -1526,25 +1213,20 @@ class ABC3D_OT_SaveFontToFile(bpy.types.Operator): if obj["font_name"] == selected_font.font_name: if butils.is_metrics_object(obj): butils.remove_faces_from_metrics(obj) - return None - bpy.app.timers.register(lambda: remove_faces(), first_interval=2) - self.report({"INFO"}, "did it") - - return {"FINISHED"} - + self.report({'INFO'}, f"did it") + return {'FINISHED'} # keep = ['io_anim_bvh', 'io_curve_svg', 'io_mesh_stl', 'io_mesh_uv_layout', 'io_scene_fbx', 'io_scene_gltf2', 'io_scene_x3d', 'cycles', 'pose_library', 'abc3d'] # for addon in keep: -# bpy.ops.preferences.addon_enable(module=addon) + # bpy.ops.preferences.addon_enable(module=addon) class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): """Create Font from selected objects""" - bl_idname = f"{__name__}.create_font_from_objects" bl_label = "Create Font" - bl_options = {"REGISTER", "UNDO"} + bl_options = {'REGISTER', 'UNDO'} font_name: bpy.props.StringProperty( default="NM_Origin", @@ -1552,6 +1234,9 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): face_name: bpy.props.StringProperty( default="Tender", ) + import_infix: bpy.props.StringProperty( + default="_NM_Origin_Tender", + ) autodetect_names: bpy.props.BoolProperty( default=True, ) @@ -1563,17 +1248,6 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): wm = context.window_manager return wm.invoke_props_dialog(self) - def do_autodetect_names(self, name: str): - ifxsplit = name.split("_") - if len(ifxsplit) < 4: - print(f"name could not be autodetected {name}") - print("split:") - print(ifxsplit) - return self.font_name, self.face_name - detected_font_name = f"{ifxsplit[1]}_{ifxsplit[2]}" - detected_face_name = ifxsplit[3] - return detected_font_name, detected_face_name - def draw(self, context): layout = self.layout if len(context.selected_objects) == 0: @@ -1581,16 +1255,13 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): layout.row().label(text="Please select your glyphs first.", icon="INFO") else: row = layout.row() - row.prop(self, "autodetect_names") - first_object_name = context.selected_objects[-1].name - if self.autodetect_names: - self.font_name, self.face_name = self.do_autodetect_names(first_object_name) + row.prop(self, 'autodetect_names') if self.autodetect_names: scale_y = 0.5 row = layout.row() row.scale_y = scale_y - row.label(text="Autodetecting names per glyph.") - row.label(text="Watch out, follow convention in naming your meshes:") + row.label( + text="Watch out, follow convention in naming your meshes:") row = layout.row() row.scale_y = scale_y row.label(text="'__'") @@ -1626,25 +1297,25 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): row.label(text="- 'colon_NM_Origin_Tender_2'") box = layout.box() box.enabled = not self.autodetect_names - box.prop(self, "font_name") - box.prop(self, "face_name") - layout.prop(self, "fix_common_misspellings") + box.prop(self, 'font_name') + box.prop(self, 'face_name') + box.prop(self, 'import_infix') + layout.prop(self, 'fix_common_misspellings') if self.fix_common_misspellings: for k in Font.known_misspellings: character = "" if Font.known_misspellings[k] in Font.name_to_glyph_d: - character = ( - f" ({Font.name_to_glyph_d[Font.known_misspellings[k]]})" - ) + character = f" ({Font.name_to_glyph_d[Font.known_misspellings[k]]})" row = layout.row() row.scale_y = 0.5 - row.label(text=f"{k} → {Font.known_misspellings[k]}{character}") + row.label( + text=f"{k} → {Font.known_misspellings[k]}{character}") def execute(self, context): print(f"executing {self.bl_idname}") if len(context.selected_objects) == 0: print(f"cancelled {self.bl_idname} - no objects selected") - return {"CANCELLED"} + return {'CANCELLED'} global shared scene = bpy.context.scene abc3d_data = scene.abc3d_data @@ -1654,6 +1325,11 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): if fontcollection is None: fontcollection = bpy.data.collections.new("ABC3D") + ifxsplit = self.import_infix.split('_') + # if len(ifxsplit) != 4: + + # font_name = f"{ifxsplit[1]}_{ifxsplit[2]}" + # face_name = ifxsplit[3] font_name = self.font_name face_name = self.face_name @@ -1666,7 +1342,13 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): print(f"processing {o.name}") process_object = True if self.autodetect_names: - font_name, face_name = self.do_autodetect_names(o.name) + ifxsplit = o.name.split('_') + if len(ifxsplit) < 4: + print( + f"whoops name could not be autodetected {o.name}") + continue + font_name = f"{ifxsplit[1]}_{ifxsplit[2]}" + face_name = ifxsplit[3] if butils.is_mesh(o) and not butils.is_metrics_object(o): uc = o.users_collection @@ -1674,24 +1356,28 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): o.name = Font.fix_glyph_name_misspellings(o.name) # name = re.sub(regex, "", o.name) # glyph_id = Font.name_to_glyph(name) - name = o.name.split("_")[0] + name = o.name.split('_')[0] glyph_id = Font.name_to_glyph(name) - o.name = f"{name}_{font_name}_{face_name}" - if glyph_id is not None: + if type(glyph_id) != type(None): o["glyph"] = glyph_id o["font_name"] = font_name o["face_name"] = face_name # butils.apply_all_transforms(o) - butils.move_in_fontcollection(o, fontcollection) + butils.move_in_fontcollection( + o, + fontcollection) Font.add_glyph( - font_name, face_name, glyph_id, bpy.types.PointerProperty(o) - ) + font_name, + face_name, + glyph_id, + bpy.types.PointerProperty(o)) # TODO: is there a better way to iterate over a CollectionProperty? found = False for f in abc3d_data.available_fonts.values(): - if f.font_name == font_name and f.face_name == face_name: + if (f.font_name == font_name + and f.face_name == face_name): found = True break if not found: @@ -1700,10 +1386,12 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): f.face_name = face_name else: - print(f"import warning: did not understand glyph {name}") - self.report({"INFO"}, f"did not understand glyph {name}") + print( + f"import warning: did not understand glyph {name}") + self.report( + {'INFO'}, f"did not understand glyph {name}") - return {"FINISHED"} + return {'FINISHED'} class ABC3D_OT_Reporter(bpy.types.Operator): @@ -1721,10 +1409,10 @@ class ABC3D_OT_Reporter(bpy.types.Operator): def execute(self, context): # this is where I send the message - self.report({"INFO"}, "whatever") + self.report({'INFO'}, 'whatever') for i in range(0, 10): - butils.ShowMessageBox("whatever", "INFO", "INFO") - return {"FINISHED"} + butils.ShowMessageBox('whatever', 'INFO', 'INFO') + return {'FINISHED'} classes = ( @@ -1742,9 +1430,6 @@ classes = ( ABC3D_PT_TextPlacement, ABC3D_PT_TextManagement, ABC3D_PT_FontCreation, - ABC3D_PG_FontCreation, - ABC3D_OT_NamingHelper, - ABC3D_PT_NamingHelper, ABC3D_PT_TextPropertiesPanel, ABC3D_OT_OpenAssetDirectory, ABC3D_OT_LoadInstalledFonts, @@ -1753,9 +1438,6 @@ classes = ( ABC3D_OT_RemoveMetrics, ABC3D_OT_AlignMetricsToActiveObject, ABC3D_OT_AlignMetrics, - ABC3D_OT_AlignOriginsToActiveObject, - # ABC3D_OT_AlignOriginsToMetrics, - # ABC3D_OT_FixObjectsMetricsOrigins, ABC3D_OT_TemporaryHelper, ABC3D_OT_RemoveText, ABC3D_OT_PlaceText, @@ -1782,7 +1464,7 @@ def compare_text_object_with_object(t, o, strict=False): if strict: return False # for p in t.keys(): - # if + # if return True @@ -1792,12 +1474,11 @@ def detect_text(): for o in scene.objects: if o[f"{utils.prefix()}_type"] == "textobject": linked_textobject = int(o[f"{utils.prefix()}_linked_textobject"]) - if ( - len(abc3d_data.available_texts) > linked_textobject - and abc3d_data.available_texts[linked_textobject].text_object == o - ): + if len(abc3d_data.available_texts) > linked_textobject \ + and abc3d_data.available_texts[linked_textobject].text_object == o: t = abc3d_data.available_texts[linked_textobject] - a = test_availability(o["font_name"], o["face_name"], o["text"]) + a = test_availability( + o["font_name"], o["face_name"], o["text"]) butils.transfer_blender_object_to_text_properties(o, t) @@ -1805,24 +1486,21 @@ def load_used_glyphs(): scene = bpy.context.scene abc3d_data = scene.abc3d_data for t in abc3d_data.available_texts: - a = Font.test_availability(t.font_name, t.face_name, t.text) + a = Font.test_availability(t.font_name, + t.face_name, + t.text) if type(a) == type(int()): if a == Font.MISSING_FONT: - butils.ShowMessageBox( - "Missing Font", - "ERROR", - [f"Font {t.font_name} is missing.", "Do you have it installed?"], - ) + butils.ShowMessageBox("Missing Font", + "ERROR", + [f"Font {t.font_name} is missing.", + "Do you have it installed?"]) if a is Font.MISSING_FACE: - butils.ShowMessageBox( - "Missing FontFace", - "ERROR", - [ - f"Font {t.font_name} is there,", - f"but the FontFace {t.face_name} is missing,", - "Do you have it installed?", - ], - ) + butils.ShowMessageBox("Missing FontFace", + "ERROR", + [f"Font {t.font_name} is there,", + f"but the FontFace {t.face_name} is missing,", + "Do you have it installed?"]) elif len(a["maybe"]) > 0: for fp in a["filepaths"]: butils.load_font_from_filepath(fp, a["maybe"]) @@ -1832,10 +1510,9 @@ def load_used_glyphs(): def load_handler(self, dummy): if not bpy.app.timers.is_registered(butils.execute_queued_functions): bpy.app.timers.register(butils.execute_queued_functions) - butils.run_in_main_thread(butils.register_installed_fonts) butils.run_in_main_thread(butils.update_available_fonts) + butils.run_in_main_thread(bpy.ops.abc3d.load_installed_fonts) butils.run_in_main_thread(load_used_glyphs) - butils.run_in_main_thread(butils.update_types) def load_handler_unload(): @@ -1849,15 +1526,11 @@ def on_frame_changed(self, dummy): # TODO PERFORMANCE: only on demand butils.set_text_on_curve(t) - depsgraph_updates_locked = False - - def unlock_depsgraph_updates(): global depsgraph_updates_locked depsgraph_updates_locked = False - def lock_depsgraph_updates(): global depsgraph_updates_locked depsgraph_updates_locked = True @@ -1865,35 +1538,25 @@ def lock_depsgraph_updates(): bpy.app.timers.unregister(unlock_depsgraph_updates) bpy.app.timers.register(unlock_depsgraph_updates, first_interval=1) - import time - - @persistent def on_depsgraph_update(scene, depsgraph): global depsgraph_updates_locked if not bpy.context.mode.startswith("EDIT") and not depsgraph_updates_locked: for u in depsgraph.updates: - if ( - f"{utils.prefix()}_linked_textobject" in u.id.keys() - and f"{utils.prefix()}_type" in u.id.keys() - and u.id[f"{utils.prefix()}_type"] == "textobject" - ): + if f"{utils.prefix()}_linked_textobject" in u.id.keys() \ + and f"{utils.prefix()}_type" in u.id.keys() \ + and u.id[f"{utils.prefix()}_type"] == 'textobject': linked_textobject = u.id[f"{utils.prefix()}_linked_textobject"] - if ( - u.is_updated_geometry - and len(scene.abc3d_data.available_texts) > linked_textobject - ): + if u.is_updated_geometry and len(scene.abc3d_data.available_texts) > linked_textobject: lock_depsgraph_updates() - def later(): - if butils.lock_depsgraph_update_n_times <= 0: + if not "lock_depsgraph_update_ntimes" in scene.abc3d_data \ + or scene.abc3d_data["lock_depsgraph_update_ntimes"] <= 0: butils.set_text_on_curve( - scene.abc3d_data.available_texts[linked_textobject] - ) - elif butils.lock_depsgraph_update_n_times <= 0: - butils.lock_depsgraph_update_n_times -= 1 - + scene.abc3d_data.available_texts[linked_textobject]) + elif scene.abc3d_data["lock_depsgraph_update_ntimes"] > 0: + scene.abc3d_data['lock_depsgraph_update_ntimes'] -= 1 butils.run_in_main_thread(later) @@ -1905,7 +1568,6 @@ def register(): addon_updater_ops.make_annotations(cls) # Avoid blender 2.8 warnings. bpy.utils.register_class(cls) bpy.types.Scene.abc3d_data = bpy.props.PointerProperty(type=ABC3D_data) - bpy.types.Scene.abc3d_font_creation = bpy.props.PointerProperty(type=ABC3D_PG_FontCreation) # bpy.types.Object.__del__ = lambda self: print(f"Bye {self.name}") # autostart if we load a blend file @@ -1914,18 +1576,19 @@ def register(): # and autostart if we reload script load_handler(None, None) - if on_frame_changed not in bpy.app.handlers.frame_change_pre: - bpy.app.handlers.frame_change_pre.append(on_frame_changed) + if on_frame_changed not in bpy.app.handlers.frame_change_post: + bpy.app.handlers.frame_change_post.append(on_frame_changed) if on_depsgraph_update not in bpy.app.handlers.depsgraph_update_post: bpy.app.handlers.depsgraph_update_post.append(on_depsgraph_update) butils.run_in_main_thread(butils.clear_available_fonts) - butils.run_in_main_thread(butils.register_installed_fonts) + # butils.run_in_main_thread(butils.load_installed_fonts) butils.run_in_main_thread(butils.update_available_fonts) - butils.run_in_main_thread(load_used_glyphs) butils.run_in_main_thread(butils.update_types) + # bpy.ops.abc3d.load_installed_fonts() + Font.init() @@ -1941,16 +1604,15 @@ def unregister(): # and when reload script load_handler_unload() - if on_frame_changed in bpy.app.handlers.frame_change_pre: - bpy.app.handlers.frame_change_pre.remove(on_frame_changed) + if on_frame_changed in bpy.app.handlers.frame_change_post: + bpy.app.handlers.frame_change_post.remove(on_frame_changed) if on_depsgraph_update in bpy.app.handlers.depsgraph_update_post: bpy.app.handlers.depsgraph_update_post.remove(on_depsgraph_update) del bpy.types.Scene.abc3d_data - del bpy.types.Scene.abc3d_font_creation print(f"UNREGISTER {utils.prefix()}") -if __name__ == "__main__": +if __name__ == '__main__': register() diff --git a/_vimrc_local.vim b/_vimrc_local.vim new file mode 100644 index 0000000..1733d50 --- /dev/null +++ b/_vimrc_local.vim @@ -0,0 +1,20 @@ +""""""""""""""""""""""""""""""""" JEDI + +let g:jedi#auto_initialization = 1 +let g:jedi#use_tabs_not_buffers = 1 +let g:jedi#environment_path = "venv" + +""""""""""""""""""""""""""""""""" ALE + +"let g:ale_python_pylint_executable = '/home/jrkb/git/pointer/neomatter/font3d/abc3d/venv/bin/pylint' +"let g:ale_python_executable='/home/jrkb/git/pointer/neomatter/font3d/abc3d/venv/bin/python' +"let g:ale_python_pylint_use_global=1 +"let g:ale_use_global_executables=1 +"let g:ale_python_auto_pipenv=1 +"let g:ale_python_auto_virtualenv=1 +"let g:ale_virtualenv_dir_names = ['venv'] + +"let g:ale_linters = { 'javascript': ['eslint', 'tsserver'], 'python': ['jedils', 'pylint', 'flake8'], 'cpp': ['cc', 'clangcheck', 'clangd', 'clangtidy', 'clazy', 'cppcheck', 'cpplint', 'cquery', 'cspell', 'flawfinder'], 'php': ['php_cs_fixer'] } +"let g:ale_fixers = { '*': ['remove_trailing_lines', 'trim_whitespace'], 'python': ['autopep8'], 'cpp': ['uncrustify'], 'javascript': js_fixers, 'css': ['prettier'], 'json': ['prettier'], 'php': ['php_cs_fixer'] } + +let g:ale_pattern_options = {'\.py$': {'ale_enabled': 0}} diff --git a/addon_updater.py b/addon_updater.py index fed01ef..3ca5a3e 100644 --- a/addon_updater.py +++ b/addon_updater.py @@ -54,8 +54,8 @@ class SingletonUpdater: needed throughout the addon. It implements all the interfaces for running updates. """ - def __init__(self): + self._engine = ForgejoEngine() self._user = None self._repo = None @@ -68,7 +68,7 @@ class SingletonUpdater: self._latest_release = None self._use_releases = False self._include_branches = False - self._include_branch_list = ["master"] + self._include_branch_list = ['master'] self._include_branch_auto_check = False self._manual_only = False self._version_min_update = None @@ -110,8 +110,7 @@ class SingletonUpdater: self._addon = __package__.lower() self._addon_package = __package__ # Must not change. self._updater_path = os.path.join( - os.path.dirname(__file__), self._addon + "_updater" - ) + os.path.dirname(__file__), self._addon + "_updater") self._addon_root = os.path.dirname(__file__) self._json = dict() self._error = None @@ -203,13 +202,11 @@ class SingletonUpdater: @property def check_interval(self): - return ( - self._check_interval_enabled, - self._check_interval_months, - self._check_interval_days, - self._check_interval_hours, - self._check_interval_minutes, - ) + return (self._check_interval_enabled, + self._check_interval_months, + self._check_interval_days, + self._check_interval_hours, + self._check_interval_minutes) @property def current_version(self): @@ -224,10 +221,12 @@ class SingletonUpdater: try: tuple(tuple_values) except: - raise ValueError("current_version must be a tuple of integers") + raise ValueError( + "current_version must be a tuple of integers") for i in tuple_values: if type(i) is not int: - raise ValueError("current_version must be a tuple of integers") + raise ValueError( + "current_version must be a tuple of integers") self._current_version = tuple(tuple_values) @property @@ -286,15 +285,15 @@ class SingletonUpdater: def include_branch_list(self, value): try: if value is None: - self._include_branch_list = ["master"] + self._include_branch_list = ['master'] elif not isinstance(value, list) or len(value) == 0: raise ValueError( - "include_branch_list should be a list of valid branches" - ) + "include_branch_list should be a list of valid branches") else: self._include_branch_list = value except: - raise ValueError("include_branch_list should be a list of valid branches") + raise ValueError( + "include_branch_list should be a list of valid branches") @property def include_branches(self): @@ -363,7 +362,8 @@ class SingletonUpdater: if value is None: self._remove_pre_update_patterns = list() elif not isinstance(value, list): - raise ValueError("remove_pre_update_patterns needs to be in a list format") + raise ValueError( + "remove_pre_update_patterns needs to be in a list format") else: self._remove_pre_update_patterns = value @@ -548,7 +548,8 @@ class SingletonUpdater: tag_names.append(tag["name"]) return tag_names - def set_check_interval(self, enabled=False, months=0, days=14, hours=0, minutes=0): + def set_check_interval(self, enabled=False, + months=0, days=14, hours=0, minutes=0): """Set the time interval between automated checks, and if enabled. Has enabled = False as default to not check against frequency, @@ -581,8 +582,7 @@ class SingletonUpdater: def __str__(self): return "Updater, with user: {a}, repository: {b}, url: {c}".format( - a=self._user, b=self._repo, c=self.form_repo_url() - ) + a=self._user, b=self._repo, c=self.form_repo_url()) # ------------------------------------------------------------------------- # API-related functions @@ -621,7 +621,10 @@ class SingletonUpdater: temp_branches.reverse() for branch in temp_branches: request = self.form_branch_url(branch) - include = {"name": branch.title(), "zipball_url": request} + include = { + "name": branch.title(), + "zipball_url": request + } self._tags = [include] + self._tags # append to front if self._tags is None: @@ -640,18 +643,13 @@ class SingletonUpdater: if not self._error: self._tag_latest = self._tags[0] branch = self._include_branch_list[0] - self.print_verbose( - "{} branch found, no releases: {}".format(branch, self._tags[0]) - ) + self.print_verbose("{} branch found, no releases: {}".format( + branch, self._tags[0])) - elif ( - ( - len(self._tags) - len(self._include_branch_list) == 0 - and self._include_branches - ) - or (len(self._tags) == 0 and not self._include_branches) - and self._prefiltered_tag_count > 0 - ): + elif ((len(self._tags) - len(self._include_branch_list) == 0 + and self._include_branches) + or (len(self._tags) == 0 and not self._include_branches) + and self._prefiltered_tag_count > 0): self._tag_latest = None self._error = "No releases available" self._error_msg = "No versions found within compatible version range" @@ -661,15 +659,13 @@ class SingletonUpdater: if not self._include_branches: self._tag_latest = self._tags[0] self.print_verbose( - "Most recent tag found:" + str(self._tags[0]["name"]) - ) + "Most recent tag found:" + str(self._tags[0]['name'])) else: # Don't return branch if in list. n = len(self._include_branch_list) self._tag_latest = self._tags[n] # guaranteed at least len()=n+1 self.print_verbose( - "Most recent tag found:" + str(self._tags[n]["name"]) - ) + "Most recent tag found:" + str(self._tags[n]['name'])) def get_raw(self, url): """All API calls to base url.""" @@ -684,12 +680,13 @@ class SingletonUpdater: # Setup private request headers if appropriate. if self._engine.token is not None: if self._engine.name == "gitlab": - request.add_header("PRIVATE-TOKEN", self._engine.token) + request.add_header('PRIVATE-TOKEN', self._engine.token) else: self.print_verbose("Tokens not setup for engine yet") # Always set user agent. - request.add_header("User-Agent", "Python/" + str(platform.python_version())) + request.add_header( + 'User-Agent', "Python/" + str(platform.python_version())) # Run the request. try: @@ -750,7 +747,8 @@ class SingletonUpdater: error = None # Make/clear the staging folder, to ensure the folder is always clean. - self.print_verbose("Preparing staging folder for download:\n" + str(local)) + self.print_verbose( + "Preparing staging folder for download:\n" + str(local)) if os.path.isdir(local): try: shutil.rmtree(local) @@ -784,16 +782,17 @@ class SingletonUpdater: # Setup private token if appropriate. if self._engine.token is not None: if self._engine.name == "gitlab": - request.add_header("PRIVATE-TOKEN", self._engine.token) + request.add_header('PRIVATE-TOKEN', self._engine.token) else: - self.print_verbose("Tokens not setup for selected engine yet") + self.print_verbose( + "Tokens not setup for selected engine yet") # Always set user agent - request.add_header("User-Agent", "Python/" + str(platform.python_version())) + request.add_header( + 'User-Agent', "Python/" + str(platform.python_version())) - self.url_retrieve( - urllib.request.urlopen(request, context=context), self._source_zip - ) + self.url_retrieve(urllib.request.urlopen(request, context=context), + self._source_zip) # Add additional checks on file size being non-zero. self.print_verbose("Successfully downloaded update zip") return True @@ -810,8 +809,7 @@ class SingletonUpdater: self.print_verbose("Backing up current addon folder") local = os.path.join(self._updater_path, "backup") tempdest = os.path.join( - self._addon_root, os.pardir, self._addon + "_updater_backup_temp" - ) + self._addon_root, os.pardir, self._addon + "_updater_backup_temp") self.print_verbose("Backup destination path: " + str(local)) @@ -820,8 +818,7 @@ class SingletonUpdater: shutil.rmtree(local) except: self.print_verbose( - "Failed to removed previous backup folder, continuing" - ) + "Failed to removed previous backup folder, continuing") self.print_trace() # Remove the temp folder. @@ -830,17 +827,16 @@ class SingletonUpdater: try: shutil.rmtree(tempdest) except: - self.print_verbose("Failed to remove existing temp folder, continuing") + self.print_verbose( + "Failed to remove existing temp folder, continuing") self.print_trace() # Make a full addon copy, temporarily placed outside the addon folder. if self._backup_ignore_patterns is not None: try: - shutil.copytree( - self._addon_root, - tempdest, - ignore=shutil.ignore_patterns(*self._backup_ignore_patterns), - ) + shutil.copytree(self._addon_root, tempdest, + ignore=shutil.ignore_patterns( + *self._backup_ignore_patterns)) except: print("Failed to create backup, still attempting update.") self.print_trace() @@ -857,8 +853,7 @@ class SingletonUpdater: # Save the date for future reference. now = datetime.now() self._json["backup_date"] = "{m}-{d}-{yr}".format( - m=now.strftime("%B"), d=now.day, yr=now.year - ) + m=now.strftime("%B"), d=now.day, yr=now.year) self.save_updater_json() def restore_backup(self): @@ -866,8 +861,7 @@ class SingletonUpdater: self.print_verbose("Restoring backup, backing up current addon folder") backuploc = os.path.join(self._updater_path, "backup") tempdest = os.path.join( - self._addon_root, os.pardir, self._addon + "_updater_backup_temp" - ) + self._addon_root, os.pardir, self._addon + "_updater_backup_temp") tempdest = os.path.abspath(tempdest) # Move instead contents back in place, instead of copy. @@ -916,8 +910,10 @@ class SingletonUpdater: self._error_msg = "Failed to create extract directory" return -1 - self.print_verbose("Begin extracting source from zip:" + str(self._source_zip)) + self.print_verbose( + "Begin extracting source from zip:" + str(self._source_zip)) with zipfile.ZipFile(self._source_zip, "r") as zfile: + if not zfile: self._error = "Install failed" self._error_msg = "Resulting file is not a zip, cannot extract" @@ -927,20 +923,19 @@ class SingletonUpdater: # Now extract directly from the first subfolder (not root) # this avoids adding the first subfolder to the path length, # which can be too long if the download has the SHA in the name. - zsep = "/" # Not using os.sep, always the / value even on windows. + zsep = '/' # Not using os.sep, always the / value even on windows. for name in zfile.namelist(): if zsep not in name: continue - top_folder = name[: name.index(zsep) + 1] + top_folder = name[:name.index(zsep) + 1] if name == top_folder + zsep: continue # skip top level folder - sub_path = name[name.index(zsep) + 1 :] + sub_path = name[name.index(zsep) + 1:] if name.endswith(zsep): try: os.mkdir(os.path.join(outdir, sub_path)) self.print_verbose( - "Extract - mkdir: " + os.path.join(outdir, sub_path) - ) + "Extract - mkdir: " + os.path.join(outdir, sub_path)) except OSError as exc: if exc.errno != errno.EEXIST: self._error = "Install failed" @@ -952,8 +947,7 @@ class SingletonUpdater: data = zfile.read(name) outfile.write(data) self.print_verbose( - "Extract - create: " + os.path.join(outdir, sub_path) - ) + "Extract - create: " + os.path.join(outdir, sub_path)) self.print_verbose("Extracted source") @@ -965,8 +959,8 @@ class SingletonUpdater: return -1 if self._subfolder_path: - self._subfolder_path.replace("/", os.path.sep) - self._subfolder_path.replace("\\", os.path.sep) + self._subfolder_path.replace('/', os.path.sep) + self._subfolder_path.replace('\\', os.path.sep) # Either directly in root of zip/one subfolder, or use specified path. if not os.path.isfile(os.path.join(unpath, "__init__.py")): @@ -1024,31 +1018,25 @@ class SingletonUpdater: # Make sure that base is not a high level shared folder, but # is dedicated just to the addon itself. self.print_verbose( - "clean=True, clearing addon folder to fresh install state" - ) + "clean=True, clearing addon folder to fresh install state") # Remove root files and folders (except update folder). - files = [ - f for f in os.listdir(base) if os.path.isfile(os.path.join(base, f)) - ] - folders = [ - f for f in os.listdir(base) if os.path.isdir(os.path.join(base, f)) - ] + files = [f for f in os.listdir(base) + if os.path.isfile(os.path.join(base, f))] + folders = [f for f in os.listdir(base) + if os.path.isdir(os.path.join(base, f))] for f in files: os.remove(os.path.join(base, f)) self.print_verbose( - "Clean removing file {}".format(os.path.join(base, f)) - ) + "Clean removing file {}".format(os.path.join(base, f))) for f in folders: if os.path.join(base, f) is self._updater_path: continue shutil.rmtree(os.path.join(base, f)) self.print_verbose( "Clean removing folder and contents {}".format( - os.path.join(base, f) - ) - ) + os.path.join(base, f))) except Exception as err: error = "failed to create clean existing addon folder" @@ -1059,9 +1047,8 @@ class SingletonUpdater: # but avoid removing/altering backup and updater file. for path, dirs, files in os.walk(base): # Prune ie skip updater folder. - dirs[:] = [ - d for d in dirs if os.path.join(path, d) not in [self._updater_path] - ] + dirs[:] = [d for d in dirs + if os.path.join(path, d) not in [self._updater_path]] for file in files: for pattern in self.remove_pre_update_patterns: if fnmatch.filter([file], pattern): @@ -1079,9 +1066,8 @@ class SingletonUpdater: # actual file copying/replacements. for path, dirs, files in os.walk(merger): # Verify structure works to prune updater sub folder overwriting. - dirs[:] = [ - d for d in dirs if os.path.join(path, d) not in [self._updater_path] - ] + dirs[:] = [d for d in dirs + if os.path.join(path, d) not in [self._updater_path]] rel_path = os.path.relpath(path, merger) dest_path = os.path.join(base, rel_path) if not os.path.exists(dest_path): @@ -1104,27 +1090,23 @@ class SingletonUpdater: os.remove(dest_file) os.rename(srcFile, dest_file) self.print_verbose( - "Overwrote file " + os.path.basename(dest_file) - ) + "Overwrote file " + os.path.basename(dest_file)) else: self.print_verbose( "Pattern not matched to {}, not overwritten".format( - os.path.basename(dest_file) - ) - ) + os.path.basename(dest_file))) else: # File did not previously exist, simply move it over. os.rename(srcFile, dest_file) - self.print_verbose("New file " + os.path.basename(dest_file)) + self.print_verbose( + "New file " + os.path.basename(dest_file)) # now remove the temp staging folder and downloaded zip try: shutil.rmtree(staging_path) except: - error = ( - "Error: Failed to remove existing staging directory, " - "consider manually removing " - ) + staging_path + error = ("Error: Failed to remove existing staging directory, " + "consider manually removing ") + staging_path self.print_verbose(error) self.print_trace() @@ -1186,12 +1168,12 @@ class SingletonUpdater: return () segments = list() - tmp = "" + tmp = '' for char in str(text): if not char.isdigit(): if len(tmp) > 0: segments.append(int(tmp)) - tmp = "" + tmp = '' else: tmp += char if len(tmp) > 0: @@ -1202,7 +1184,7 @@ class SingletonUpdater: if not self._include_branches: return () else: - return text + return (text) return tuple(segments) def check_for_update_async(self, callback=None): @@ -1211,8 +1193,7 @@ class SingletonUpdater: self._json is not None and "update_ready" in self._json and self._json["version_text"] != dict() - and self._json["update_ready"] - ) + and self._json["update_ready"]) if is_ready: self._update_ready = True @@ -1229,13 +1210,15 @@ class SingletonUpdater: self.print_verbose("Skipping async check, already started") # already running the bg thread elif self._update_ready is None: - print("{} updater: Running background check for update".format(self.addon)) + print("{} updater: Running background check for update".format( + self.addon)) self.start_async_check_update(False, callback) def check_for_update_now(self, callback=None): self._error = None self._error_msg = None - self.print_verbose("Check update pressed, first getting current status") + self.print_verbose( + "Check update pressed, first getting current status") if self._async_checking: self.print_verbose("Skipping async check, already started") return # already running the bg thread @@ -1260,7 +1243,9 @@ class SingletonUpdater: # avoid running again in, just return past result if found # but if force now check, then still do it if self._update_ready is not None and not now: - return (self._update_ready, self._update_version, self._update_link) + return (self._update_ready, + self._update_version, + self._update_link) if self._current_version is None: raise ValueError("current_version not yet defined") @@ -1274,18 +1259,22 @@ class SingletonUpdater: self.set_updater_json() # self._json if not now and not self.past_interval_timestamp(): - self.print_verbose("Aborting check for updated, check interval not reached") + self.print_verbose( + "Aborting check for updated, check interval not reached") return (False, None, None) # check if using tags or releases # note that if called the first time, this will pull tags from online if self._fake_install: - self.print_verbose("fake_install = True, setting fake version as ready") + self.print_verbose( + "fake_install = True, setting fake version as ready") self._update_ready = True self._update_version = "(999,999,999)" self._update_link = "http://127.0.0.1" - return (self._update_ready, self._update_version, self._update_link) + return (self._update_ready, + self._update_version, + self._update_link) # Primary internet call, sets self._tags and self._tag_latest. self.get_tags() @@ -1338,6 +1327,7 @@ class SingletonUpdater: else: # Situation where branches not included. if new_version > self._current_version: + self._update_ready = True self._update_version = new_version self._update_link = link @@ -1396,7 +1386,8 @@ class SingletonUpdater: if self._fake_install: # Change to True, to trigger the reload/"update installed" handler. self.print_verbose("fake_install=True") - self.print_verbose("Just reloading and running any handler triggers") + self.print_verbose( + "Just reloading and running any handler triggers") self._json["just_updated"] = True self.save_updater_json() if self._backup_current is True: @@ -1410,16 +1401,15 @@ class SingletonUpdater: self.print_verbose("Update stopped, new version not ready") if callback: callback( - self._addon_package, "Update stopped, new version not ready" - ) + self._addon_package, + "Update stopped, new version not ready") return "Update stopped, new version not ready" elif self._update_link is None: # this shouldn't happen if update is ready self.print_verbose("Update stopped, update link unavailable") if callback: - callback( - self._addon_package, "Update stopped, update link unavailable" - ) + callback(self._addon_package, + "Update stopped, update link unavailable") return "Update stopped, update link unavailable" if revert_tag is None: @@ -1471,12 +1461,12 @@ class SingletonUpdater: return True now = datetime.now() - last_check = datetime.strptime(self._json["last_check"], "%Y-%m-%d %H:%M:%S.%f") + last_check = datetime.strptime( + self._json["last_check"], "%Y-%m-%d %H:%M:%S.%f") offset = timedelta( days=self._check_interval_days + 30 * self._check_interval_months, hours=self._check_interval_hours, - minutes=self._check_interval_minutes, - ) + minutes=self._check_interval_minutes) delta = (now - offset) - last_check if delta.total_seconds() > 0: @@ -1492,8 +1482,8 @@ class SingletonUpdater: Will also rename old file paths to addon-specific path if found. """ json_path = os.path.join( - self._updater_path, "{}_updater_status.json".format(self._addon_package) - ) + self._updater_path, + "{}_updater_status.json".format(self._addon_package)) old_json_path = os.path.join(self._updater_path, "updater_status.json") # Rename old file if it exists. @@ -1527,7 +1517,7 @@ class SingletonUpdater: "ignore": False, "just_restored": False, "just_updated": False, - "version_text": dict(), + "version_text": dict() } self.save_updater_json() @@ -1547,13 +1537,11 @@ class SingletonUpdater: jpath = self.get_json_path() if not os.path.isdir(os.path.dirname(jpath)): - print( - "State error: Directory does not exist, cannot save json: ", - os.path.basename(jpath), - ) + print("State error: Directory does not exist, cannot save json: ", + os.path.basename(jpath)) return try: - with open(jpath, "w") as outf: + with open(jpath, 'w') as outf: data_out = json.dumps(self._json, indent=4) outf.write(data_out) except: @@ -1587,13 +1575,8 @@ class SingletonUpdater: if self._async_checking: return self.print_verbose("Starting background checking thread") - check_thread = threading.Thread( - target=self.async_check_update, - args=( - now, - callback, - ), - ) + check_thread = threading.Thread(target=self.async_check_update, + args=(now, callback,)) check_thread.daemon = True self._check_thread = check_thread check_thread.start() @@ -1647,19 +1630,17 @@ class SingletonUpdater: # Updater Engines # ----------------------------------------------------------------------------- - class BitbucketEngine: """Integration to Bitbucket API for git-formatted repositories""" def __init__(self): - self.api_url = "https://api.bitbucket.org" + self.api_url = 'https://api.bitbucket.org' self.token = None self.name = "bitbucket" def form_repo_url(self, updater): return "{}/2.0/repositories/{}/{}".format( - self.api_url, updater.user, updater.repo - ) + self.api_url, updater.user, updater.repo) def form_tags_url(self, updater): return self.form_repo_url(updater) + "/refs/tags?sort=-name" @@ -1669,28 +1650,31 @@ class BitbucketEngine: def get_zip_url(self, name, updater): return "https://bitbucket.org/{user}/{repo}/get/{name}.zip".format( - user=updater.user, repo=updater.repo, name=name - ) + user=updater.user, + repo=updater.repo, + name=name) def parse_tags(self, response, updater): if response is None: return list() return [ - {"name": tag["name"], "zipball_url": self.get_zip_url(tag["name"], updater)} - for tag in response["values"] - ] + { + "name": tag["name"], + "zipball_url": self.get_zip_url(tag["name"], updater) + } for tag in response["values"]] class GithubEngine: """Integration to Github API""" def __init__(self): - self.api_url = "https://api.github.com" + self.api_url = 'https://api.github.com' self.token = None self.name = "github" def form_repo_url(self, updater): - return "{}/repos/{}/{}".format(self.api_url, updater.user, updater.repo) + return "{}/repos/{}/{}".format( + self.api_url, updater.user, updater.repo) def form_tags_url(self, updater): if updater.use_releases: @@ -1714,7 +1698,7 @@ class GitlabEngine: """Integration to GitLab API""" def __init__(self): - self.api_url = "https://gitlab.com" + self.api_url = 'https://gitlab.com' self.token = None self.name = "gitlab" @@ -1726,19 +1710,19 @@ class GitlabEngine: def form_branch_list_url(self, updater): # does not validate branch name. - return "{}/repository/branches".format(self.form_repo_url(updater)) + return "{}/repository/branches".format( + self.form_repo_url(updater)) def form_branch_url(self, branch, updater): # Could clash with tag names and if it does, it will download TAG zip # instead of branch zip to get direct path, would need. return "{}/repository/archive.zip?sha={}".format( - self.form_repo_url(updater), branch - ) + self.form_repo_url(updater), branch) def get_zip_url(self, sha, updater): return "{base}/repository/archive.zip?sha={sha}".format( - base=self.form_repo_url(updater), sha=sha - ) + base=self.form_repo_url(updater), + sha=sha) # def get_commit_zip(self, id, updater): # return self.form_repo_url(updater)+"/repository/archive.zip?sha:"+id @@ -1749,11 +1733,8 @@ class GitlabEngine: return [ { "name": tag["name"], - "zipball_url": self.get_zip_url(tag["commit"]["id"], updater), - } - for tag in response - ] - + "zipball_url": self.get_zip_url(tag["commit"]["id"], updater) + } for tag in response] class ForgejoEngine: """Integration to Forgejo/Gitea API""" @@ -1761,7 +1742,7 @@ class ForgejoEngine: def __init__(self): # the api_url may be overwritten by form_repo_url # if updater.host is set - self.api_url = "https://codeberg.org" + self.api_url = 'https://codeberg.org' self.token = None self.name = "forgejo" @@ -1775,17 +1756,19 @@ class ForgejoEngine: def form_branch_list_url(self, updater): # does not validate branch name. - return "{}/branches".format(self.form_repo_url(updater)) + return "{}/branches".format( + self.form_repo_url(updater)) def form_branch_url(self, branch, updater): # Could clash with tag names and if it does, it will download TAG zip # instead of branch zip to get direct path, would need. - return "{}/archive/{}.zip".format(self.form_repo_url(updater), branch) + return "{}/archive/{}.zip".format( + self.form_repo_url(updater), branch) def get_zip_url(self, sha, updater): return "{base}/archive/{sha}.zip".format( - base=self.form_repo_url(updater), sha=sha - ) + base=self.form_repo_url(updater), + sha=sha) # def get_commit_zip(self, id, updater): # return self.form_repo_url(updater)+"/repository/archive.zip?sha:"+id @@ -1796,11 +1779,8 @@ class ForgejoEngine: return [ { "name": tag["name"], - "zipball_url": self.get_zip_url(tag["commit"]["sha"], updater), - } - for tag in response - ] - + "zipball_url": self.get_zip_url(tag["commit"]["sha"], updater) + } for tag in response] # ----------------------------------------------------------------------------- # The module-shared class instance, diff --git a/addon_updater_ops.py b/addon_updater_ops.py index 0316cc0..0c3e108 100644 --- a/addon_updater_ops.py +++ b/addon_updater_ops.py @@ -83,17 +83,15 @@ def make_annotations(cls): if not hasattr(bpy.app, "version") or bpy.app.version < (2, 80): return cls if bpy.app.version < (2, 93, 0): - bl_props = {k: v for k, v in cls.__dict__.items() if isinstance(v, tuple)} + bl_props = {k: v for k, v in cls.__dict__.items() + if isinstance(v, tuple)} else: - bl_props = { - k: v - for k, v in cls.__dict__.items() - if isinstance(v, bpy.props._PropertyDeferred) - } + bl_props = {k: v for k, v in cls.__dict__.items() + if isinstance(v, bpy.props._PropertyDeferred)} if bl_props: - if "__annotations__" not in cls.__dict__: - setattr(cls, "__annotations__", {}) - annotations = cls.__dict__["__annotations__"] + if '__annotations__' not in cls.__dict__: + setattr(cls, '__annotations__', {}) + annotations = cls.__dict__['__annotations__'] for k, v in bl_props.items(): annotations[k] = v delattr(cls, k) @@ -131,23 +129,20 @@ def get_user_preferences(context=None): # Simple popup to prompt use to check for update & offer install if available. class AddonUpdaterInstallPopup(bpy.types.Operator): """Check and install update if available""" - bl_label = "Update {x} addon".format(x=updater.addon) bl_idname = updater.addon + ".updater_install_popup" bl_description = "Popup to check and display current updates available" - bl_options = {"REGISTER", "INTERNAL"} + bl_options = {'REGISTER', 'INTERNAL'} # if true, run clean install - ie remove all files before adding new # equivalent to deleting the addon and reinstalling, except the # updater folder/backup folder remains clean_install = bpy.props.BoolProperty( name="Clean install", - description=( - "If enabled, completely clear the addon's folder before " - "installing new update, creating a fresh install" - ), + description=("If enabled, completely clear the addon's folder before " + "installing new update, creating a fresh install"), default=False, - options={"HIDDEN"}, + options={'HIDDEN'} ) ignore_enum = bpy.props.EnumProperty( @@ -156,9 +151,9 @@ class AddonUpdaterInstallPopup(bpy.types.Operator): items=[ ("install", "Update Now", "Install update now"), ("ignore", "Ignore", "Ignore this update to prevent future popups"), - ("defer", "Defer", "Defer choice till next blender session"), + ("defer", "Defer", "Defer choice till next blender session") ], - options={"HIDDEN"}, + options={'HIDDEN'} ) def check(self, context): @@ -175,11 +170,10 @@ class AddonUpdaterInstallPopup(bpy.types.Operator): elif updater.update_ready: col = layout.column() col.scale_y = 0.7 - col.label( - text="Update {} ready!".format(updater.update_version), - icon="LOOP_FORWARDS", - ) - col.label(text="Choose 'Update Now' & press OK to install, ", icon="BLANK1") + col.label(text="Update {} ready!".format(updater.update_version), + icon="LOOP_FORWARDS") + col.label(text="Choose 'Update Now' & press OK to install, ", + icon="BLANK1") col.label(text="or click outside window to defer", icon="BLANK1") row = col.row() row.prop(self, "ignore_enum", expand=True) @@ -200,21 +194,22 @@ class AddonUpdaterInstallPopup(bpy.types.Operator): def execute(self, context): # In case of error importing updater. if updater.invalid_updater: - return {"CANCELLED"} + return {'CANCELLED'} if updater.manual_only: bpy.ops.wm.url_open(url=updater.website) elif updater.update_ready: - # Action based on enum selection. - if self.ignore_enum == "defer": - return {"FINISHED"} - elif self.ignore_enum == "ignore": - updater.ignore_update() - return {"FINISHED"} - res = updater.run_update( - force=False, callback=post_update_callback, clean=self.clean_install - ) + # Action based on enum selection. + if self.ignore_enum == 'defer': + return {'FINISHED'} + elif self.ignore_enum == 'ignore': + updater.ignore_update() + return {'FINISHED'} + + res = updater.run_update(force=False, + callback=post_update_callback, + clean=self.clean_install) # Should return 0, if not something happened. if updater.verbose: @@ -227,86 +222,84 @@ class AddonUpdaterInstallPopup(bpy.types.Operator): # Re-launch this dialog. atr = AddonUpdaterInstallPopup.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]), atr[1])("INVOKE_DEFAULT") + getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT') else: updater.print_verbose("Doing nothing, not ready for update") - return {"FINISHED"} + return {'FINISHED'} # User preference check-now operator class AddonUpdaterCheckNow(bpy.types.Operator): bl_label = "Check now for " + updater.addon + " update" bl_idname = updater.addon + ".updater_check_now" - bl_description = "Check now for an update to the {} addon".format(updater.addon) - bl_options = {"REGISTER", "INTERNAL"} + bl_description = "Check now for an update to the {} addon".format( + updater.addon) + bl_options = {'REGISTER', 'INTERNAL'} def execute(self, context): if updater.invalid_updater: - return {"CANCELLED"} + return {'CANCELLED'} if updater.async_checking and updater.error is None: # Check already happened. # Used here to just avoid constant applying settings below. # Ignoring if error, to prevent being stuck on the error screen. - return {"CANCELLED"} + return {'CANCELLED'} # apply the UI settings settings = get_user_preferences(context) if not settings: updater.print_verbose( - "Could not get {} preferences, update check skipped".format(__package__) - ) - return {"CANCELLED"} + "Could not get {} preferences, update check skipped".format( + __package__)) + return {'CANCELLED'} updater.set_check_interval( enabled=settings.auto_check_update, months=settings.updater_interval_months, days=settings.updater_interval_days, hours=settings.updater_interval_hours, - minutes=settings.updater_interval_minutes, - ) + minutes=settings.updater_interval_minutes) # Input is an optional callback function. This function should take a # bool input. If true: update ready, if false: no update ready. updater.check_for_update_now(ui_refresh) - return {"FINISHED"} + return {'FINISHED'} class AddonUpdaterUpdateNow(bpy.types.Operator): bl_label = "Update " + updater.addon + " addon now" bl_idname = updater.addon + ".updater_update_now" bl_description = "Update to the latest version of the {x} addon".format( - x=updater.addon - ) - bl_options = {"REGISTER", "INTERNAL"} + x=updater.addon) + bl_options = {'REGISTER', 'INTERNAL'} # If true, run clean install - ie remove all files before adding new # equivalent to deleting the addon and reinstalling, except the updater # folder/backup folder remains. clean_install = bpy.props.BoolProperty( name="Clean install", - description=( - "If enabled, completely clear the addon's folder before " - "installing new update, creating a fresh install" - ), + description=("If enabled, completely clear the addon's folder before " + "installing new update, creating a fresh install"), default=False, - options={"HIDDEN"}, + options={'HIDDEN'} ) def execute(self, context): + # in case of error importing updater if updater.invalid_updater: - return {"CANCELLED"} + return {'CANCELLED'} if updater.manual_only: bpy.ops.wm.url_open(url=updater.website) if updater.update_ready: # if it fails, offer to open the website instead try: - res = updater.run_update( - force=False, callback=post_update_callback, clean=self.clean_install - ) + res = updater.run_update(force=False, + callback=post_update_callback, + clean=self.clean_install) # Should return 0, if not something happened. if updater.verbose: @@ -319,30 +312,30 @@ class AddonUpdaterUpdateNow(bpy.types.Operator): updater._error_msg = str(expt) updater.print_trace() atr = AddonUpdaterInstallManually.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]), atr[1])("INVOKE_DEFAULT") + getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT') elif updater.update_ready is None: (update_ready, version, link) = updater.check_for_update(now=True) # Re-launch this dialog. atr = AddonUpdaterInstallPopup.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]), atr[1])("INVOKE_DEFAULT") + getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT') elif not updater.update_ready: - self.report({"INFO"}, "Nothing to update") - return {"CANCELLED"} + self.report({'INFO'}, "Nothing to update") + return {'CANCELLED'} else: - self.report({"ERROR"}, "Encountered a problem while trying to update") - return {"CANCELLED"} + self.report( + {'ERROR'}, "Encountered a problem while trying to update") + return {'CANCELLED'} - return {"FINISHED"} + return {'FINISHED'} class AddonUpdaterUpdateTarget(bpy.types.Operator): bl_label = updater.addon + " version target" bl_idname = updater.addon + ".updater_update_target" bl_description = "Install a targeted version of the {x} addon".format( - x=updater.addon - ) - bl_options = {"REGISTER", "INTERNAL"} + x=updater.addon) + bl_options = {'REGISTER', 'INTERNAL'} def target_version(self, context): # In case of error importing updater. @@ -359,7 +352,7 @@ class AddonUpdaterUpdateTarget(bpy.types.Operator): target = bpy.props.EnumProperty( name="Target version to install", description="Select the version to install", - items=target_version, + items=target_version ) # If true, run clean install - ie remove all files before adding new @@ -367,12 +360,10 @@ class AddonUpdaterUpdateTarget(bpy.types.Operator): # updater folder/backup folder remains. clean_install = bpy.props.BoolProperty( name="Clean install", - description=( - "If enabled, completely clear the addon's folder before " - "installing new update, creating a fresh install" - ), + description=("If enabled, completely clear the addon's folder before " + "installing new update, creating a fresh install"), default=False, - options={"HIDDEN"}, + options={'HIDDEN'} ) @classmethod @@ -398,35 +389,36 @@ class AddonUpdaterUpdateTarget(bpy.types.Operator): def execute(self, context): # In case of error importing updater. if updater.invalid_updater: - return {"CANCELLED"} + return {'CANCELLED'} res = updater.run_update( force=False, revert_tag=self.target, callback=post_update_callback, - clean=self.clean_install, - ) + clean=self.clean_install) # Should return 0, if not something happened. if res == 0: updater.print_verbose("Updater returned successful") else: - updater.print_verbose("Updater returned {}, , error occurred".format(res)) - return {"CANCELLED"} + updater.print_verbose( + "Updater returned {}, , error occurred".format(res)) + return {'CANCELLED'} - return {"FINISHED"} + return {'FINISHED'} class AddonUpdaterInstallManually(bpy.types.Operator): """As a fallback, direct the user to download the addon manually""" - bl_label = "Install update manually" bl_idname = updater.addon + ".updater_install_manually" bl_description = "Proceed to manually install update" - bl_options = {"REGISTER", "INTERNAL"} + bl_options = {'REGISTER', 'INTERNAL'} error = bpy.props.StringProperty( - name="Error Occurred", default="", options={"HIDDEN"} + name="Error Occurred", + default="", + options={'HIDDEN'} ) def invoke(self, context, event): @@ -443,8 +435,10 @@ class AddonUpdaterInstallManually(bpy.types.Operator): if self.error != "": col = layout.column() col.scale_y = 0.7 - col.label(text="There was an issue trying to auto-install", icon="ERROR") - col.label(text="Press the download button below and install", icon="BLANK1") + col.label(text="There was an issue trying to auto-install", + icon="ERROR") + col.label(text="Press the download button below and install", + icon="BLANK1") col.label(text="the zip file like a normal addon.", icon="BLANK1") else: col = layout.column() @@ -460,10 +454,12 @@ class AddonUpdaterInstallManually(bpy.types.Operator): if updater.update_link is not None: row.operator( - "wm.url_open", text="Direct download" - ).url = updater.update_link + "wm.url_open", + text="Direct download").url = updater.update_link else: - row.operator("wm.url_open", text="(failed to retrieve direct download)") + row.operator( + "wm.url_open", + text="(failed to retrieve direct download)") row.enabled = False if updater.website is not None: @@ -475,19 +471,20 @@ class AddonUpdaterInstallManually(bpy.types.Operator): row.label(text="See source website to download the update") def execute(self, context): - return {"FINISHED"} + return {'FINISHED'} class AddonUpdaterUpdatedSuccessful(bpy.types.Operator): """Addon in place, popup telling user it completed or what went wrong""" - bl_label = "Installation Report" bl_idname = updater.addon + ".updater_update_successful" bl_description = "Update installation response" - bl_options = {"REGISTER", "INTERNAL", "UNDO"} + bl_options = {'REGISTER', 'INTERNAL', 'UNDO'} error = bpy.props.StringProperty( - name="Error Occurred", default="", options={"HIDDEN"} + name="Error Occurred", + default="", + options={'HIDDEN'} ) def invoke(self, context, event): @@ -513,8 +510,9 @@ class AddonUpdaterUpdatedSuccessful(bpy.types.Operator): rw = col.row() rw.scale_y = 2 rw.operator( - "wm.url_open", text="Click for manual download.", icon="BLANK1" - ).url = updater.website + "wm.url_open", + text="Click for manual download.", + icon="BLANK1").url = updater.website elif not updater.auto_reload_post_update: # Tell user to restart blender after an update/restore! if "just_restored" in saved and saved["just_restored"]: @@ -523,17 +521,20 @@ class AddonUpdaterUpdatedSuccessful(bpy.types.Operator): alert_row = col.row() alert_row.alert = True alert_row.operator( - "wm.quit_blender", text="Restart blender to reload", icon="BLANK1" - ) + "wm.quit_blender", + text="Restart blender to reload", + icon="BLANK1") updater.json_reset_restore() else: col = layout.column() - col.label(text="Addon successfully installed", icon="FILE_TICK") + col.label( + text="Addon successfully installed", icon="FILE_TICK") alert_row = col.row() alert_row.alert = True alert_row.operator( - "wm.quit_blender", text="Restart blender to reload", icon="BLANK1" - ) + "wm.quit_blender", + text="Restart blender to reload", + icon="BLANK1") else: # reload addon, but still recommend they restart blender @@ -542,28 +543,28 @@ class AddonUpdaterUpdatedSuccessful(bpy.types.Operator): col.scale_y = 0.7 col.label(text="Addon restored", icon="RECOVER_LAST") col.label( - text="Consider restarting blender to fully reload.", icon="BLANK1" - ) + text="Consider restarting blender to fully reload.", + icon="BLANK1") updater.json_reset_restore() else: col = layout.column() col.scale_y = 0.7 - col.label(text="Addon successfully installed", icon="FILE_TICK") col.label( - text="Consider restarting blender to fully reload.", icon="BLANK1" - ) + text="Addon successfully installed", icon="FILE_TICK") + col.label( + text="Consider restarting blender to fully reload.", + icon="BLANK1") def execute(self, context): - return {"FINISHED"} + return {'FINISHED'} class AddonUpdaterRestoreBackup(bpy.types.Operator): """Restore addon from backup""" - bl_label = "Restore backup" bl_idname = updater.addon + ".updater_restore_backup" bl_description = "Restore addon from backup" - bl_options = {"REGISTER", "INTERNAL"} + bl_options = {'REGISTER', 'INTERNAL'} @classmethod def poll(cls, context): @@ -575,18 +576,17 @@ class AddonUpdaterRestoreBackup(bpy.types.Operator): def execute(self, context): # in case of error importing updater if updater.invalid_updater: - return {"CANCELLED"} + return {'CANCELLED'} updater.restore_backup() - return {"FINISHED"} + return {'FINISHED'} class AddonUpdaterIgnore(bpy.types.Operator): """Ignore update to prevent future popups""" - bl_label = "Ignore update" bl_idname = updater.addon + ".updater_ignore" bl_description = "Ignore update to prevent future popups" - bl_options = {"REGISTER", "INTERNAL"} + bl_options = {'REGISTER', 'INTERNAL'} @classmethod def poll(cls, context): @@ -600,26 +600,25 @@ class AddonUpdaterIgnore(bpy.types.Operator): def execute(self, context): # in case of error importing updater if updater.invalid_updater: - return {"CANCELLED"} + return {'CANCELLED'} updater.ignore_update() self.report({"INFO"}, "Open addon preferences for updater options") - return {"FINISHED"} + return {'FINISHED'} class AddonUpdaterEndBackground(bpy.types.Operator): """Stop checking for update in the background""" - bl_label = "End background check" bl_idname = updater.addon + ".end_background_check" bl_description = "Stop checking for update in the background" - bl_options = {"REGISTER", "INTERNAL"} + bl_options = {'REGISTER', 'INTERNAL'} def execute(self, context): # in case of error importing updater if updater.invalid_updater: - return {"CANCELLED"} + return {'CANCELLED'} updater.stop_async_check_update() - return {"FINISHED"} + return {'FINISHED'} # ----------------------------------------------------------------------------- @@ -646,16 +645,16 @@ def updater_run_success_popup_handler(scene): try: if "scene_update_post" in dir(bpy.app.handlers): - bpy.app.handlers.scene_update_post.remove(updater_run_success_popup_handler) + bpy.app.handlers.scene_update_post.remove( + updater_run_success_popup_handler) else: bpy.app.handlers.depsgraph_update_post.remove( - updater_run_success_popup_handler - ) + updater_run_success_popup_handler) except: pass atr = AddonUpdaterUpdatedSuccessful.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]), atr[1])("INVOKE_DEFAULT") + getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT') @persistent @@ -670,11 +669,11 @@ def updater_run_install_popup_handler(scene): try: if "scene_update_post" in dir(bpy.app.handlers): - bpy.app.handlers.scene_update_post.remove(updater_run_install_popup_handler) + bpy.app.handlers.scene_update_post.remove( + updater_run_install_popup_handler) else: bpy.app.handlers.depsgraph_update_post.remove( - updater_run_install_popup_handler - ) + updater_run_install_popup_handler) except: pass @@ -688,12 +687,12 @@ def updater_run_install_popup_handler(scene): # User probably manually installed to get the up to date addon # in here. Clear out the update flag using this function. updater.print_verbose( - "{} updater: appears user updated, clearing flag".format(updater.addon) - ) + "{} updater: appears user updated, clearing flag".format( + updater.addon)) updater.json_reset_restore() return atr = AddonUpdaterInstallPopup.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]), atr[1])("INVOKE_DEFAULT") + getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT') def background_update_callback(update_ready): @@ -721,9 +720,11 @@ def background_update_callback(update_ready): return if "scene_update_post" in dir(bpy.app.handlers): # 2.7x - bpy.app.handlers.scene_update_post.append(updater_run_install_popup_handler) + bpy.app.handlers.scene_update_post.append( + updater_run_install_popup_handler) else: # 2.8+ - bpy.app.handlers.depsgraph_update_post.append(updater_run_install_popup_handler) + bpy.app.handlers.depsgraph_update_post.append( + updater_run_install_popup_handler) ran_auto_check_install_popup = True updater.print_verbose("Attempted popup prompt") @@ -747,18 +748,17 @@ def post_update_callback(module_name, res=None): # This is the same code as in conditional at the end of the register # function, ie if "auto_reload_post_update" == True, skip code. updater.print_verbose( - "{} updater: Running post update callback".format(updater.addon) - ) + "{} updater: Running post update callback".format(updater.addon)) atr = AddonUpdaterUpdatedSuccessful.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]), atr[1])("INVOKE_DEFAULT") + getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT') global ran_update_success_popup ran_update_success_popup = True else: # Some kind of error occurred and it was unable to install, offer # manual download instead. atr = AddonUpdaterUpdatedSuccessful.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]), atr[1])("INVOKE_DEFAULT", error=res) + getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT', error=res) return @@ -791,13 +791,11 @@ def check_for_update_background(): settings = get_user_preferences(bpy.context) if not settings: return - updater.set_check_interval( - enabled=settings.auto_check_update, - months=settings.updater_interval_months, - days=settings.updater_interval_days, - hours=settings.updater_interval_hours, - minutes=settings.updater_interval_minutes, - ) + updater.set_check_interval(enabled=settings.auto_check_update, + months=settings.updater_interval_months, + days=settings.updater_interval_days, + hours=settings.updater_interval_hours, + minutes=settings.updater_interval_minutes) # Input is an optional callback function. This function should take a bool # input, if true: update ready, if false: no update ready. @@ -815,25 +813,22 @@ def check_for_update_nonthreaded(self, context): settings = get_user_preferences(bpy.context) if not settings: if updater.verbose: - print( - "Could not get {} preferences, update check skipped".format(__package__) - ) + print("Could not get {} preferences, update check skipped".format( + __package__)) return - updater.set_check_interval( - enabled=settings.auto_check_update, - months=settings.updater_interval_months, - days=settings.updater_interval_days, - hours=settings.updater_interval_hours, - minutes=settings.updater_interval_minutes, - ) + updater.set_check_interval(enabled=settings.auto_check_update, + months=settings.updater_interval_months, + days=settings.updater_interval_days, + hours=settings.updater_interval_hours, + minutes=settings.updater_interval_minutes) (update_ready, version, link) = updater.check_for_update(now=False) if update_ready: atr = AddonUpdaterInstallPopup.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]), atr[1])("INVOKE_DEFAULT") + getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT') else: updater.print_verbose("No update ready") - self.report({"INFO"}, "No update ready") + self.report({'INFO'}, "No update ready") def show_reload_popup(): @@ -871,9 +866,11 @@ def show_reload_popup(): return if "scene_update_post" in dir(bpy.app.handlers): # 2.7x - bpy.app.handlers.scene_update_post.append(updater_run_success_popup_handler) + bpy.app.handlers.scene_update_post.append( + updater_run_success_popup_handler) else: # 2.8+ - bpy.app.handlers.depsgraph_update_post.append(updater_run_success_popup_handler) + bpy.app.handlers.depsgraph_update_post.append( + updater_run_success_popup_handler) ran_update_success_popup = True @@ -899,7 +896,10 @@ def update_notice_box_ui(self, context): col = box.column() alert_row = col.row() alert_row.alert = True - alert_row.operator("wm.quit_blender", text="Restart blender", icon="ERROR") + alert_row.operator( + "wm.quit_blender", + text="Restart blender", + icon="ERROR") col.label(text="to complete update") return @@ -924,13 +924,13 @@ def update_notice_box_ui(self, context): colR = split.column(align=True) colR.scale_y = 1.5 if not updater.manual_only: - colR.operator( - AddonUpdaterUpdateNow.bl_idname, text="Update", icon="LOOP_FORWARDS" - ) + colR.operator(AddonUpdaterUpdateNow.bl_idname, + text="Update", icon="LOOP_FORWARDS") col.operator("wm.url_open", text="Open website").url = updater.website # ops = col.operator("wm.url_open",text="Direct download") # ops.url=updater.update_link - col.operator(AddonUpdaterInstallManually.bl_idname, text="Install manually") + col.operator(AddonUpdaterInstallManually.bl_idname, + text="Install manually") else: # ops = col.operator("wm.url_open", text="Direct download") # ops.url=updater.update_link @@ -959,7 +959,7 @@ def update_settings_ui(self, context, element=None): return settings = get_user_preferences(context) if not settings: - box.label(text="Error getting updater preferences", icon="ERROR") + box.label(text="Error getting updater preferences", icon='ERROR') return # auto-update settings @@ -971,11 +971,9 @@ def update_settings_ui(self, context, element=None): saved_state = updater.json if "just_updated" in saved_state and saved_state["just_updated"]: row.alert = True - row.operator( - "wm.quit_blender", - text="Restart blender to complete update", - icon="ERROR", - ) + row.operator("wm.quit_blender", + text="Restart blender to complete update", + icon="ERROR") return split = layout_split(row, factor=0.4) @@ -1009,13 +1007,16 @@ def update_settings_ui(self, context, element=None): split.scale_y = 2 if "ssl" in updater.error_msg.lower(): split.enabled = True - split.operator(AddonUpdaterInstallManually.bl_idname, text=updater.error) + split.operator(AddonUpdaterInstallManually.bl_idname, + text=updater.error) else: split.enabled = False - split.operator(AddonUpdaterCheckNow.bl_idname, text=updater.error) + split.operator(AddonUpdaterCheckNow.bl_idname, + text=updater.error) split = sub_col.split(align=True) split.scale_y = 2 - split.operator(AddonUpdaterCheckNow.bl_idname, text="", icon="FILE_REFRESH") + split.operator(AddonUpdaterCheckNow.bl_idname, + text="", icon="FILE_REFRESH") elif updater.update_ready is None and not updater.async_checking: col.scale_y = 2 @@ -1031,62 +1032,61 @@ def update_settings_ui(self, context, element=None): split.scale_y = 2 split.operator(AddonUpdaterEndBackground.bl_idname, text="", icon="X") - elif ( - updater.include_branches - and len(updater.tags) == len(updater.include_branch_list) - and not updater.manual_only - ): + elif updater.include_branches and \ + len(updater.tags) == len(updater.include_branch_list) and not \ + updater.manual_only: # No releases found, but still show the appropriate branch. sub_col = col.row(align=True) sub_col.scale_y = 1 split = sub_col.split(align=True) split.scale_y = 2 - update_now_txt = "Update directly to {}".format(updater.include_branch_list[0]) + update_now_txt = "Update directly to {}".format( + updater.include_branch_list[0]) split.operator(AddonUpdaterUpdateNow.bl_idname, text=update_now_txt) split = sub_col.split(align=True) split.scale_y = 2 - split.operator(AddonUpdaterCheckNow.bl_idname, text="", icon="FILE_REFRESH") + split.operator(AddonUpdaterCheckNow.bl_idname, + text="", icon="FILE_REFRESH") elif updater.update_ready and not updater.manual_only: sub_col = col.row(align=True) sub_col.scale_y = 1 split = sub_col.split(align=True) split.scale_y = 2 - split.operator( - AddonUpdaterUpdateNow.bl_idname, - text="Update now to " + str(updater.update_version), - ) + split.operator(AddonUpdaterUpdateNow.bl_idname, + text="Update now to " + str(updater.update_version)) split = sub_col.split(align=True) split.scale_y = 2 - split.operator(AddonUpdaterCheckNow.bl_idname, text="", icon="FILE_REFRESH") + split.operator(AddonUpdaterCheckNow.bl_idname, + text="", icon="FILE_REFRESH") elif updater.update_ready and updater.manual_only: col.scale_y = 2 dl_now_txt = "Download " + str(updater.update_version) - col.operator("wm.url_open", text=dl_now_txt).url = updater.website + col.operator("wm.url_open", + text=dl_now_txt).url = updater.website else: # i.e. that updater.update_ready == False. sub_col = col.row(align=True) sub_col.scale_y = 1 split = sub_col.split(align=True) split.enabled = False split.scale_y = 2 - split.operator(AddonUpdaterCheckNow.bl_idname, text="Addon is up to date") + split.operator(AddonUpdaterCheckNow.bl_idname, + text="Addon is up to date") split = sub_col.split(align=True) split.scale_y = 2 - split.operator(AddonUpdaterCheckNow.bl_idname, text="", icon="FILE_REFRESH") + split.operator(AddonUpdaterCheckNow.bl_idname, + text="", icon="FILE_REFRESH") if not updater.manual_only: col = row.column(align=True) if updater.include_branches and len(updater.include_branch_list) > 0: branch = updater.include_branch_list[0] - col.operator( - AddonUpdaterUpdateTarget.bl_idname, - text="Install {} / old version".format(branch), - ) + col.operator(AddonUpdaterUpdateTarget.bl_idname, + text="Install {} / old version".format(branch)) else: - col.operator( - AddonUpdaterUpdateTarget.bl_idname, text="(Re)install addon version" - ) + col.operator(AddonUpdaterUpdateTarget.bl_idname, + text="(Re)install addon version") last_date = "none found" backup_path = os.path.join(updater.stage_path, "backup") if "backup_date" in updater.json and os.path.isdir(backup_path): @@ -1103,7 +1103,7 @@ def update_settings_ui(self, context, element=None): if updater.error is not None and updater.error_msg is not None: row.label(text=updater.error_msg) elif last_check: - last_check = last_check[0 : last_check.index(".")] + last_check = last_check[0: last_check.index(".")] row.label(text="Last update check: " + last_check) else: row.label(text="Last update check: Never") @@ -1127,7 +1127,7 @@ def update_settings_ui_condensed(self, context, element=None): return settings = get_user_preferences(context) if not settings: - row.label(text="Error getting updater preferences", icon="ERROR") + row.label(text="Error getting updater preferences", icon='ERROR') return # Special case to tell user to restart blender, if set that way. @@ -1138,8 +1138,7 @@ def update_settings_ui_condensed(self, context, element=None): row.operator( "wm.quit_blender", text="Restart blender to complete update", - icon="ERROR", - ) + icon="ERROR") return col = row.column() @@ -1150,13 +1149,16 @@ def update_settings_ui_condensed(self, context, element=None): split.scale_y = 2 if "ssl" in updater.error_msg.lower(): split.enabled = True - split.operator(AddonUpdaterInstallManually.bl_idname, text=updater.error) + split.operator(AddonUpdaterInstallManually.bl_idname, + text=updater.error) else: split.enabled = False - split.operator(AddonUpdaterCheckNow.bl_idname, text=updater.error) + split.operator(AddonUpdaterCheckNow.bl_idname, + text=updater.error) split = sub_col.split(align=True) split.scale_y = 2 - split.operator(AddonUpdaterCheckNow.bl_idname, text="", icon="FILE_REFRESH") + split.operator(AddonUpdaterCheckNow.bl_idname, + text="", icon="FILE_REFRESH") elif updater.update_ready is None and not updater.async_checking: col.scale_y = 2 @@ -1172,11 +1174,9 @@ def update_settings_ui_condensed(self, context, element=None): split.scale_y = 2 split.operator(AddonUpdaterEndBackground.bl_idname, text="", icon="X") - elif ( - updater.include_branches - and len(updater.tags) == len(updater.include_branch_list) - and not updater.manual_only - ): + elif updater.include_branches and \ + len(updater.tags) == len(updater.include_branch_list) and not \ + updater.manual_only: # No releases found, but still show the appropriate branch. sub_col = col.row(align=True) sub_col.scale_y = 1 @@ -1186,20 +1186,20 @@ def update_settings_ui_condensed(self, context, element=None): split.operator(AddonUpdaterUpdateNow.bl_idname, text=now_txt) split = sub_col.split(align=True) split.scale_y = 2 - split.operator(AddonUpdaterCheckNow.bl_idname, text="", icon="FILE_REFRESH") + split.operator(AddonUpdaterCheckNow.bl_idname, + text="", icon="FILE_REFRESH") elif updater.update_ready and not updater.manual_only: sub_col = col.row(align=True) sub_col.scale_y = 1 split = sub_col.split(align=True) split.scale_y = 2 - split.operator( - AddonUpdaterUpdateNow.bl_idname, - text="Update now to " + str(updater.update_version), - ) + split.operator(AddonUpdaterUpdateNow.bl_idname, + text="Update now to " + str(updater.update_version)) split = sub_col.split(align=True) split.scale_y = 2 - split.operator(AddonUpdaterCheckNow.bl_idname, text="", icon="FILE_REFRESH") + split.operator(AddonUpdaterCheckNow.bl_idname, + text="", icon="FILE_REFRESH") elif updater.update_ready and updater.manual_only: col.scale_y = 2 @@ -1211,10 +1211,12 @@ def update_settings_ui_condensed(self, context, element=None): split = sub_col.split(align=True) split.enabled = False split.scale_y = 2 - split.operator(AddonUpdaterCheckNow.bl_idname, text="Addon is up to date") + split.operator(AddonUpdaterCheckNow.bl_idname, + text="Addon is up to date") split = sub_col.split(align=True) split.scale_y = 2 - split.operator(AddonUpdaterCheckNow.bl_idname, text="", icon="FILE_REFRESH") + split.operator(AddonUpdaterCheckNow.bl_idname, + text="", icon="FILE_REFRESH") row = element.row() row.prop(settings, "auto_check_update") @@ -1225,7 +1227,7 @@ def update_settings_ui_condensed(self, context, element=None): if updater.error is not None and updater.error_msg is not None: row.label(text=updater.error_msg) elif last_check != "" and last_check is not None: - last_check = last_check[0 : last_check.index(".")] + last_check = last_check[0: last_check.index(".")] row.label(text="Last check: " + last_check) else: row.label(text="Last check: Never") @@ -1326,7 +1328,7 @@ classes = ( AddonUpdaterUpdatedSuccessful, AddonUpdaterRestoreBackup, AddonUpdaterIgnore, - AddonUpdaterEndBackground, + AddonUpdaterEndBackground ) @@ -1394,13 +1396,7 @@ def register(bl_info): updater.backup_current = True # True by default # Sample ignore patterns for when creating backup of current during update. - updater.backup_ignore_patterns = [ - ".git", - "__pycache__", - "*.bat", - ".gitignore", - "*.exe", - ] + updater.backup_ignore_patterns = [".git", "__pycache__", "*.bat", ".gitignore", "*.exe"] # Alternate example patterns: # updater.backup_ignore_patterns = [".git", "__pycache__", "*.bat", ".gitignore", "*.exe"] @@ -1469,7 +1465,7 @@ def register(bl_info): # Note: updater.include_branch_list defaults to ['master'] branch if set to # none. Example targeting another multiple branches allowed to pull from: # updater.include_branch_list = ['master', 'dev'] - updater.include_branch_list = ["main", "dev"] # None is the equivalent = ['master'] + updater.include_branch_list = ['main', 'dev'] # None is the equivalent = ['master'] # Only allow manual install, thus prompting the user to open # the addon's web page to download, specifically: updater.website diff --git a/bimport.py b/bimport.py index cad66de..2ec6f58 100644 --- a/bimport.py +++ b/bimport.py @@ -1,21 +1,20 @@ import bpy -from bpy.props import ( - StringProperty, - BoolProperty, - EnumProperty, - IntProperty, - CollectionProperty, -) +from bpy.props import (StringProperty, + BoolProperty, + EnumProperty, + IntProperty, + FloatProperty, + CollectionProperty) from bpy.types import Operator -from bpy_extras.io_utils import ImportHelper -from io_scene_gltf2 import ConvertGLTF2_Base +from bpy_extras.io_utils import ImportHelper, ExportHelper +from io_scene_gltf2 import ConvertGLTF2_Base import importlib # then import dependencies for our addon if "Font" in locals(): importlib.reload(Font) else: - pass + from .common import Font if "utils" in locals(): importlib.reload(utils) @@ -23,52 +22,43 @@ else: from .common import utils -# taken from blender_git/blender/scripts/addons/io_scene_gltf2/__init__.py - +# taken from blender_git/blender/scripts/addons/io_scene_gltf2/__init__.py def get_font_faces_in_file(filepath): from io_scene_gltf2.io.imp.gltf2_io_gltf import glTFImporter, ImportError - + try: - import_settings = {"import_user_extensions": []} + import_settings = { 'import_user_extensions': [] } gltf_importer = glTFImporter(filepath, import_settings) gltf_importer.read() gltf_importer.checks() - + out = [] for node in gltf_importer.data.nodes: - if ( - type(node.extras) != type(None) - and "glyph" in node.extras - and not ("type" in node.extras and node.extras["type"] == "metrics") - and not ( - f"{utils.prefix()}_type" in node.extras - and node.extras[f"{utils.prefix()}_type"] == "metrics" - ) - ): + if type(node.extras) != type(None) \ + and "glyph" in node.extras \ + and not ("type" in node.extras and node.extras["type"] == "metrics") \ + and not (f"{utils.prefix()}_type" in node.extras and node.extras[f"{utils.prefix()}_type"] == "metrics"): out.append(node.extras) return out - - except ImportError: + + except ImportError as e: return None - -# taken from blender_git/blender/scripts/addons/io_scene_gltf2/__init__.py - +# taken from blender_git/blender/scripts/addons/io_scene_gltf2/__init__.py class GetFontFacesInFile(Operator, ImportHelper): """Load a glTF 2.0 font and check which faces are in there""" - - bl_idname = "abc3d.check_font_gltf" - bl_label = "Check glTF 2.0 Font" - bl_options = {"REGISTER", "UNDO"} + bl_idname = f"abc3d.check_font_gltf" + bl_label = 'Check glTF 2.0 Font' + bl_options = {'REGISTER', 'UNDO'} files: CollectionProperty( name="File Path", type=bpy.types.OperatorFileListElement, ) - # bpy.ops.abc3d.check_font_gltf(filepath="/home/jrkb/.config/blender/4.1/datafiles/abc3d/fonts/JRKB_LOL.glb") +# bpy.ops.abc3d.check_font_gltf(filepath="/home/jrkb/.config/blender/4.1/datafiles/abc3d/fonts/JRKB_LOL.glb") found_fonts = [] def execute(self, context): @@ -76,109 +66,100 @@ class GetFontFacesInFile(Operator, ImportHelper): def check_gltf2(self, context): import os + import sys if self.files: # Multiple file check - ret = {"CANCELLED"} + ret = {'CANCELLED'} dirname = os.path.dirname(self.filepath) for file in self.files: path = os.path.join(dirname, file.name) - if self.unit_check(path) == {"FINISHED"}: - ret = {"FINISHED"} + if self.unit_check(path) == {'FINISHED'}: + ret = {'FINISHED'} return ret else: # Single file check return self.unit_check(self.filepath) def unit_check(self, filename): - self.found_fonts.append(["LOL", "WHATEVER"]) - return {"FINISHED"} - + self.found_fonts.append(["LOL","WHATEVER"]) + return {'FINISHED'} class ImportGLTF2(Operator, ConvertGLTF2_Base, ImportHelper): """Load a glTF 2.0 font""" + bl_idname = f"abc3d.import_font_gltf" + bl_label = 'Import glTF 2.0 Font' + bl_options = {'REGISTER', 'UNDO'} - bl_idname = "abc3d.import_font_gltf" - bl_label = "Import glTF 2.0 Font" - bl_options = {"REGISTER", "UNDO"} - - filter_glob: StringProperty(default="*.glb;*.gltf", options={"HIDDEN"}) + filter_glob: StringProperty(default="*.glb;*.gltf", options={'HIDDEN'}) files: CollectionProperty( name="File Path", type=bpy.types.OperatorFileListElement, ) - loglevel: IntProperty(name="Log Level", description="Log Level") + loglevel: IntProperty( + name='Log Level', + description="Log Level") import_pack_images: BoolProperty( - name="Pack Images", description="Pack all images into .blend file", default=True + name='Pack Images', + description='Pack all images into .blend file', + default=True ) merge_vertices: BoolProperty( - name="Merge Vertices", + name='Merge Vertices', description=( - "The glTF format requires discontinuous normals, UVs, and " - "other vertex attributes to be stored as separate vertices, " - "as required for rendering on typical graphics hardware. " - "This option attempts to combine co-located vertices where possible. " - "Currently cannot combine verts with different normals" + 'The glTF format requires discontinuous normals, UVs, and ' + 'other vertex attributes to be stored as separate vertices, ' + 'as required for rendering on typical graphics hardware. ' + 'This option attempts to combine co-located vertices where possible. ' + 'Currently cannot combine verts with different normals' ), default=False, ) import_shading: EnumProperty( name="Shading", - items=( - ("NORMALS", "Use Normal Data", ""), - ("FLAT", "Flat Shading", ""), - ("SMOOTH", "Smooth Shading", ""), - ), + items=(("NORMALS", "Use Normal Data", ""), + ("FLAT", "Flat Shading", ""), + ("SMOOTH", "Smooth Shading", "")), description="How normals are computed during import", - default="NORMALS", - ) + default="NORMALS") bone_heuristic: EnumProperty( name="Bone Dir", items=( - ( - "BLENDER", - "Blender (best for import/export round trip)", + ("BLENDER", "Blender (best for import/export round trip)", "Good for re-importing glTFs exported from Blender, " "and re-exporting glTFs to glTFs after Blender editing. " - "Bone tips are placed on their local +Y axis (in glTF space)", - ), - ( - "TEMPERANCE", - "Temperance (average)", + "Bone tips are placed on their local +Y axis (in glTF space)"), + ("TEMPERANCE", "Temperance (average)", "Decent all-around strategy. " "A bone with one child has its tip placed on the local axis " - "closest to its child", - ), - ( - "FORTUNE", - "Fortune (may look better, less accurate)", + "closest to its child"), + ("FORTUNE", "Fortune (may look better, less accurate)", "Might look better than Temperance, but also might have errors. " "A bone with one child has its tip placed at its child's root. " - "Non-uniform scalings may get messed up though, so beware", - ), + "Non-uniform scalings may get messed up though, so beware"), ), description="Heuristic for placing bones. Tries to make bones pretty", default="BLENDER", ) guess_original_bind_pose: BoolProperty( - name="Guess Original Bind Pose", + name='Guess Original Bind Pose', description=( - "Try to guess the original bind pose for skinned meshes from " - "the inverse bind matrices. " - "When off, use default/rest pose as bind pose" + 'Try to guess the original bind pose for skinned meshes from ' + 'the inverse bind matrices. ' + 'When off, use default/rest pose as bind pose' ), default=True, ) import_webp_texture: BoolProperty( - name="Import WebP textures", + name='Import WebP textures', description=( "If a texture exists in WebP format, " "loads the WebP texture instead of the fallback PNG/JPEG one" @@ -187,7 +168,7 @@ class ImportGLTF2(Operator, ConvertGLTF2_Base, ImportHelper): ) glyphs: StringProperty( - name="Import only these glyphs", + name='Import only these glyphs', description=( "Loading glyphs is expensive, if the meshes are huge" "So we can filter all glyphs out that we do not want" @@ -216,32 +197,25 @@ class ImportGLTF2(Operator, ConvertGLTF2_Base, ImportHelper): layout.use_property_split = True layout.use_property_decorate = False # No animation. - layout.prop(self, "import_pack_images") - layout.prop(self, "merge_vertices") - layout.prop(self, "import_shading") - layout.prop(self, "guess_original_bind_pose") - layout.prop(self, "bone_heuristic") - layout.prop(self, "export_import_convert_lighting_mode") - layout.prop(self, "import_webp_texture") + layout.prop(self, 'import_pack_images') + layout.prop(self, 'merge_vertices') + layout.prop(self, 'import_shading') + layout.prop(self, 'guess_original_bind_pose') + layout.prop(self, 'bone_heuristic') + layout.prop(self, 'export_import_convert_lighting_mode') + layout.prop(self, 'import_webp_texture') def invoke(self, context, event): import sys - preferences = bpy.context.preferences for addon_name in preferences.addons.keys(): try: - if hasattr( - sys.modules[addon_name], "glTF2ImportUserExtension" - ) or hasattr(sys.modules[addon_name], "glTF2ImportUserExtensions"): - importer_extension_panel_unregister_functors.append( - sys.modules[addon_name].register_panel() - ) + if hasattr(sys.modules[addon_name], 'glTF2ImportUserExtension') or hasattr(sys.modules[addon_name], 'glTF2ImportUserExtensions'): + importer_extension_panel_unregister_functors.append(sys.modules[addon_name].register_panel()) except Exception: pass - self.has_active_importer_extensions = ( - len(importer_extension_panel_unregister_functors) > 0 - ) + self.has_active_importer_extensions = len(importer_extension_panel_unregister_functors) > 0 return ImportHelper.invoke(self, context, event) def execute(self, context): @@ -256,26 +230,25 @@ class ImportGLTF2(Operator, ConvertGLTF2_Base, ImportHelper): user_extensions = [] import sys - preferences = bpy.context.preferences for addon_name in preferences.addons.keys(): try: module = sys.modules[addon_name] except Exception: continue - if hasattr(module, "glTF2ImportUserExtension"): + if hasattr(module, 'glTF2ImportUserExtension'): extension_ctor = module.glTF2ImportUserExtension user_extensions.append(extension_ctor()) - import_settings["import_user_extensions"] = user_extensions + import_settings['import_user_extensions'] = user_extensions if self.files: # Multiple file import - ret = {"CANCELLED"} + ret = {'CANCELLED'} dirname = os.path.dirname(self.filepath) for file in self.files: path = os.path.join(dirname, file.name) - if self.unit_import(path, import_settings) == {"FINISHED"}: - ret = {"FINISHED"} + if self.unit_import(path, import_settings) == {'FINISHED'}: + ret = {'FINISHED'} return ret else: # Single file import @@ -283,18 +256,11 @@ class ImportGLTF2(Operator, ConvertGLTF2_Base, ImportHelper): def unit_import(self, filename, import_settings): import time - try: - from io_scene_gltf2.io.imp.gltf2_io_gltf import glTFImporter, ImportError - from io_scene_gltf2.blender.imp.gltf2_blender_gltf import BlenderGlTF - from io_scene_gltf2.blender.imp.gltf2_blender_vnode import VNode, compute_vnodes - from io_scene_gltf2.blender.com.gltf2_blender_extras import set_extras - from io_scene_gltf2.blender.imp.gltf2_blender_node import BlenderNode - except (ModuleNotFoundError, ImportError): - from io_scene_gltf2.io.imp.blender_gltf import glTFImporter, ImportError - from io_scene_gltf2.blender.imp.blender_gltf import BlenderGlTF - from io_scene_gltf2.blender.imp.vnode import VNode, compute_vnodes - from io_scene_gltf2.blender.com.extras import set_extras - from io_scene_gltf2.blender.imp.node import BlenderNode + from io_scene_gltf2.io.imp.gltf2_io_gltf import glTFImporter, ImportError + from io_scene_gltf2.blender.imp.gltf2_blender_gltf import BlenderGlTF + from io_scene_gltf2.blender.imp.gltf2_blender_vnode import VNode, compute_vnodes + from io_scene_gltf2.blender.com.gltf2_blender_extras import set_extras + from io_scene_gltf2.blender.imp.gltf2_blender_node import BlenderNode try: gltf = glTFImporter(filename, import_settings) @@ -342,31 +308,18 @@ class ImportGLTF2(Operator, ConvertGLTF2_Base, ImportHelper): # indeed representing a glyph we want for node in gltf.data.nodes: # :-O woah - if ( - type(node.extras) != type(None) - and "glyph" in node.extras - and (node.extras["glyph"] in self.glyphs or len(self.glyphs) == 0) - and ( - self.font_name == "" - or ( - "font_name" in node.extras - and ( - node.extras["font_name"] in self.font_name - or len(self.glyphs) == 0 - ) - ) - ) - and ( - self.face_name == "" - or ( - "face_name" in node.extras - and ( - node.extras["face_name"] in self.face_name - or len(self.glyphs) == 0 - ) - ) - ) - ): + if type(node.extras) != type(None) \ + and "glyph" in node.extras \ + and (node.extras["glyph"] in self.glyphs \ + or len(self.glyphs) == 0) \ + and (self.font_name == "" or \ + ( "font_name" in node.extras \ + and (node.extras["font_name"] in self.font_name \ + or len(self.glyphs) == 0))) \ + and (self.face_name == "" or \ + ( "face_name" in node.extras \ + and (node.extras["face_name"] in self.face_name \ + or len(self.glyphs) == 0))): # if there is a match, add the node incl children .. add_node(node) # .. and their parents recursively @@ -402,7 +355,7 @@ class ImportGLTF2(Operator, ConvertGLTF2_Base, ImportHelper): # and some have different indices for node in nodes: if type(node.children) != type(None): - children = [] # brand new children + children = [] # brand new children for i, c in enumerate(node.children): # check if children are lost if c in node_indices: @@ -446,26 +399,23 @@ class ImportGLTF2(Operator, ConvertGLTF2_Base, ImportHelper): vnode = gltf.vnodes[vi] if vnode.type == VNode.Object: if vnode.parent is not None: - if not hasattr(gltf.vnodes[vnode.parent], "blender_object"): - create_blender_object(gltf, vnode.parent, nodes) - if not hasattr(vnode, "blender_object"): + if not hasattr(gltf.vnodes[vnode.parent], + "blender_object"): + create_blender_object(gltf, + vnode.parent, + nodes) + if not hasattr(vnode, + "blender_object"): obj = BlenderNode.create_object(gltf, vi) obj["font_import"] = True n_vars = vars(nodes[vi]) if "extras" in n_vars: set_extras(obj, n_vars["extras"]) - if ( - "glyph" in n_vars["extras"] - and not ( - "type" in n_vars["extras"] - and n_vars["extras"]["type"] == "metrics" - ) - and not ( - f"{utils.prefix()}_type" in n_vars["extras"] - and n_vars["extras"][f"{utils.prefix()}_type"] - == "metrics" - ) - ): + if "glyph" in n_vars["extras"] and \ + not ("type" in n_vars["extras"] and \ + n_vars["extras"]["type"] == "metrics") and \ + not (f"{utils.prefix()}_type" in n_vars["extras"] and \ + n_vars["extras"][f"{utils.prefix()}_type"] == "metrics"): obj["type"] = "glyph" for vi, vnode in gltf.vnodes.items(): @@ -482,15 +432,14 @@ class ImportGLTF2(Operator, ConvertGLTF2_Base, ImportHelper): if hasattr(gltf.log.logger, "removeHandler"): gltf.log.logger.removeHandler(gltf.log_handler) - return {"FINISHED"} + return {'FINISHED'} except ImportError as e: - self.report({"ERROR"}, e.args[0]) - return {"CANCELLED"} + self.report({'ERROR'}, e.args[0]) + return {'CANCELLED'} def set_debug_log(self): import logging - if bpy.app.debug_value == 0: self.loglevel = logging.CRITICAL elif bpy.app.debug_value == 1: diff --git a/butils.py b/butils.py index 9b696d1..b9488ba 100644 --- a/butils.py +++ b/butils.py @@ -2,6 +2,7 @@ import importlib import os import queue import re +from multiprocessing import Process import bpy import mathutils @@ -20,22 +21,18 @@ else: from .common import utils execution_queue = queue.Queue() -lock_depsgraph_update_n_times = -1 - # This function can safely be called in another thread. # The function will be executed when the timer runs the next time. def run_in_main_thread(function): execution_queue.put(function) - def execute_queued_functions(): while not execution_queue.empty(): function = execution_queue.get() function() return 1.0 - def apply_all_transforms(obj): mb = obj.matrix_basis if hasattr(obj.data, "transform"): @@ -44,18 +41,16 @@ def apply_all_transforms(obj): c.matrix_local = mb @ c.matrix_local obj.matrix_basis.identity() - def get_parent_collection_names(collection, parent_names): - for parent_collection in bpy.data.collections: - if collection.name in parent_collection.children.keys(): - parent_names.append(parent_collection.name) - get_parent_collection_names(parent_collection, parent_names) - return - + for parent_collection in bpy.data.collections: + if collection.name in parent_collection.children.keys(): + parent_names.append(parent_collection.name) + get_parent_collection_names(parent_collection, parent_names) + return # Ensure it's a curve object # TODO: no raising, please -def get_curve_length(curve_obj, resolution=-1): +def get_curve_length(curve_obj, resolution = -1): total_length = 0 curve = curve_obj.data @@ -63,20 +58,16 @@ def get_curve_length(curve_obj, resolution=-1): # Loop through all splines in the curve for spline in curve.splines: total_length = total_length + spline.calc_length(resolution=resolution) - + return total_length - -def get_curve_line_lengths(curve_obj, resolution=-1): +def get_curve_line_lengths(curve_obj, resolution = -1): lengths = [] for spline in curve_obj.data.splines: lengths.append(spline.calc_length(resolution=resolution)) return lengths - -def get_next_line_advance( - curve_obj, current_advance, previous_glyph_advance, resolution=-1 -): +def get_next_line_advance(curve_obj, current_advance, previous_glyph_advance, resolution = -1): curve_line_lengths = get_curve_line_lengths(curve_obj, resolution) total_length = 0 for cll in curve_line_lengths: @@ -85,37 +76,30 @@ def get_next_line_advance( return total_length return current_advance - def calc_point_on_bezier(bezier_point_1, bezier_point_2, t): p1 = bezier_point_1.co h1 = bezier_point_1.handle_right p2 = bezier_point_2.co h2 = bezier_point_2.handle_left - return ( - ((1 - t) ** 3) * p1 - + (3 * t * (1 - t) ** 2) * h1 - + (3 * (t**2) * (1 - t)) * h2 - + (t**3) * p2 - ) - + return ((1 - t)**3) * p1 + (3 * t * (1 - t)**2) * h1 + (3 * (t**2) * (1 - t)) * h2 + (t**3) * p2 # same in slightly more lines # result is equal, performance minimally better perhaps? # def calc_point_on_bezier(bezier_point_1, bezier_point_2, ratio): -# startPoint = bezier_point_1.co -# controlPoint1 = bezier_point_1.handle_right -# controlPoint2 = bezier_point_2.handle_left -# endPoint = bezier_point_2.co + # startPoint = bezier_point_1.co + # controlPoint1 = bezier_point_1.handle_right + # controlPoint2 = bezier_point_2.handle_left + # endPoint = bezier_point_2.co -# remainder = 1 - ratio -# ratioSquared = ratio * ratio -# remainderSquared = remainder * remainder -# startPointMultiplier = remainderSquared * remainder -# controlPoint1Multiplier = remainderSquared * ratio * 3 -# controlPoint2Multiplier = ratioSquared * remainder * 3 -# endPointMultiplier = ratioSquared * ratio + # remainder = 1 - ratio + # ratioSquared = ratio * ratio + # remainderSquared = remainder * remainder + # startPointMultiplier = remainderSquared * remainder + # controlPoint1Multiplier = remainderSquared * ratio * 3 + # controlPoint2Multiplier = ratioSquared * remainder * 3 + # endPointMultiplier = ratioSquared * ratio -# return startPoint * startPointMultiplier + controlPoint1 * controlPoint1Multiplier + controlPoint2 * controlPoint2Multiplier + endPoint * endPointMultiplier + # return startPoint * startPointMultiplier + controlPoint1 * controlPoint1Multiplier + controlPoint2 * controlPoint2Multiplier + endPoint * endPointMultiplier def calc_tangent_on_bezier(bezier_point_1, bezier_point_2, t): @@ -124,21 +108,15 @@ def calc_tangent_on_bezier(bezier_point_1, bezier_point_2, t): p2 = bezier_point_2.co h2 = bezier_point_2.handle_left return ( - (-3 * (1 - t) ** 2) * p1 - + (-6 * t * (1 - t) + 3 * (1 - t) ** 2) * h1 - + (-3 * (t**2) + 6 * t * (1 - t)) * h2 - + (3 * t**2) * p2 - ).normalized() + (-3 * (1 - t)**2) * p1 + (-6 * t * (1 - t) + 3 * (1 - t)**2) * h1 + + (-3 * (t**2) + 6 * t * (1 - t)) * h2 + (3 * t**2) * p2 + ).normalized() + +from math import acos, pi, radians, sqrt - - -def align_rotations_auto_pivot( - mask, input_rotations, vectors, factors, local_main_axis -): - output_rotations = [ - mathutils.Matrix().to_3x3() for _ in range(len(input_rotations)) - ] +def align_rotations_auto_pivot(mask, input_rotations, vectors, factors, local_main_axis): + output_rotations = [mathutils.Matrix().to_3x3() for _ in range(len(input_rotations))] for i in mask: vector = mathutils.Vector(vectors[i]).normalized() @@ -157,12 +135,10 @@ def align_rotations_auto_pivot( if rotation_axis.length < 1e-6: # Vectors are linearly dependent, fallback to another axis rotation_axis = (old_axis + mathutils.Matrix().to_3x3().col[2]).normalized() - + if rotation_axis.length < 1e-6: # This is now guaranteed to not be zero - rotation_axis = ( - -(old_axis) + mathutils.Matrix().to_3x3().col[1] - ).normalized() + rotation_axis = (-(old_axis) + mathutils.Matrix().to_3x3().col[1]).normalized() # full_angle = radians(sqrt((4 * pow(input_rotation.to_quaternion().dot(mathutils.Quaternion(vectors[i].normalized())), 2) - 3))) # dot = old_axis.dot(new_axis) @@ -177,9 +153,8 @@ def align_rotations_auto_pivot( return [mat.to_4x4() for mat in output_rotations] - def calc_bezier_length(bezier_point_1, bezier_point_2, resolution=20): - step = 1 / resolution + step = 1/resolution previous_p = bezier_point_1.co length = 0 for i in range(-1, resolution): @@ -189,30 +164,28 @@ def calc_bezier_length(bezier_point_1, bezier_point_2, resolution=20): previous_p = p return length - -def calc_point_on_bezier_spline( - bezier_spline_obj, distance, output_tangent=False, resolution_factor=1.0 -): +def calc_point_on_bezier_spline(bezier_spline_obj, + distance, + output_tangent = False, + resolution_factor = 1.0): # what's the point of just one point # assert len(bezier_spline_obj.bezier_points) >= 2 # however, maybe let's have it not crash and do this if len(bezier_spline_obj.bezier_points) < 1: - print( - "butils::calc_point_on_bezier_spline: whoops, no points. panicking. return 0,0,0" - ) + print("butils::calc_point_on_bezier_spline: whoops, no points. panicking. return 0,0,0") if output_tangent: - return mathutils.Vector((0, 0, 0)), mathutils.Vector((1, 0, 0)) + return mathutils.Vector((0,0,0)), mathutils.Vector((1,0,0)) else: - return mathutils.Vector((0, 0, 0)) + return mathutils.Vector((0,0,0)) if len(bezier_spline_obj.bezier_points) == 1: p = bezier_spline_obj.bezier_points[0] travel = (p.handle_left - p.co).normalized() * distance if output_tangent: - tangent = mathutils.Vector((1, 0, 0)) + tangent = mathutils.Vector((1,0,0)) return travel, tangent else: return travel - + if distance <= 0: p = bezier_spline_obj.bezier_points[0] travel = (p.co - p.handle_left).normalized() * distance @@ -228,36 +201,31 @@ def calc_point_on_bezier_spline( lengths = [] total_length = 0 n_bezier_points = len(bezier_spline_obj.bezier_points) - real_n_bezier_points = len(bezier_spline_obj.bezier_points) - if bezier_spline_obj.use_cyclic_u: - n_bezier_points += 1 - for i in range(0, n_bezier_points - 1): - i_a = i % (n_bezier_points - 1) - i_b = (i_a + 1) % real_n_bezier_points - bezier = [ - bezier_spline_obj.bezier_points[i_a], - bezier_spline_obj.bezier_points[i_b], - ] - length = calc_bezier_length( - bezier[0], - bezier[1], - int(bezier_spline_obj.resolution_u * resolution_factor), - ) - total_length += length - beziers.append(bezier) - lengths.append(length) - # if total_length > distance: - # break + for i in range(0, len(bezier_spline_obj.bezier_points) - 1): + bezier = [ bezier_spline_obj.bezier_points[i], + bezier_spline_obj.bezier_points[i + 1] ] + length = calc_bezier_length(bezier[0], + bezier[1], + int(bezier_spline_obj.resolution_u * resolution_factor)) + total_length += length + beziers.append(bezier) + lengths.append(length) + # if total_length > distance: + # break iterated_distance = 0 for i in range(0, len(beziers)): if iterated_distance + lengths[i] > distance: - distance_on_bezier = distance - iterated_distance + distance_on_bezier = (distance - iterated_distance) d = distance_on_bezier / lengths[i] # print(f"i: {i}, d: {d}, distance_on_bezier: {distance_on_bezier}, distance: {distance}") - location = calc_point_on_bezier(beziers[i][0], beziers[i][1], d) + location = calc_point_on_bezier(beziers[i][0], + beziers[i][1], + d) if output_tangent: - tangent = calc_tangent_on_bezier(beziers[i][0], beziers[i][1], d) + tangent = calc_tangent_on_bezier(beziers[i][0], + beziers[i][1], + d) return location, tangent else: return location @@ -268,19 +236,19 @@ def calc_point_on_bezier_spline( travel = (p.handle_right - p.co).normalized() * (distance - total_length) location = p.co + travel if output_tangent: - tangent = calc_tangent_on_bezier(beziers[last_i][0], p, 1) + tangent = calc_tangent_on_bezier(beziers[last_i][0], + p, + 1) return location, tangent else: return location -def calc_point_on_bezier_curve( - bezier_curve_obj, - distance, - output_tangent=False, - output_spline_index=False, - resolution_factor=1.0, -): +def calc_point_on_bezier_curve(bezier_curve_obj, + distance, + output_tangent = False, + output_spline_index = False, + resolution_factor = 1.0): curve = bezier_curve_obj.data # Loop through all splines in the curve @@ -292,36 +260,36 @@ def calc_point_on_bezier_curve( if output_spline_index and output_tangent: # return value from c_p_o_b_s is a tuple # so we need to append tuple + tuple - return calc_point_on_bezier_spline( - spline, (distance - total_length), output_tangent, resolution_factor - ) + (i,) + return calc_point_on_bezier_spline(spline, + (distance - total_length), + output_tangent, + resolution_factor) + (i,) if output_spline_index and not output_tangent: # return value from c_p_o_b_s is a location vector # so we need to append with a comma - return ( - calc_point_on_bezier_spline( - spline, - (distance - total_length), - output_tangent, - resolution_factor, - ), - i, - ) + return calc_point_on_bezier_spline(spline, + (distance - total_length), + output_tangent, + resolution_factor), i else: - return calc_point_on_bezier_spline( - spline, (distance - total_length), output_tangent, resolution_factor - ) + return calc_point_on_bezier_spline(spline, + (distance - total_length), + output_tangent, + resolution_factor) total_length += length - # NOTE: this is a fallback - # and should not happen usually - return bezier_curve_obj.matrix_world @ mathutils.Vector((distance, 0, 0)) + # TODO: can this fail? + # def get_objects_by_name(name, startswith="", endswith=""): -# return [obj for obj in bpy.context.scene.objects if obj.name.startswith(startswith) and if obj.name.endswith(endswith)] + # return [obj for obj in bpy.context.scene.objects if obj.name.startswith(startswith) and if obj.name.endswith(endswith)] - -def find_objects_by_name(objects, equals="", contains="", startswith="", endswith=""): +def find_objects_by_name( + objects, + equals="", + contains="", + startswith="", + endswith=""): # handle equals if equals != "": index = objects.find(equals) @@ -329,55 +297,48 @@ def find_objects_by_name(objects, equals="", contains="", startswith="", endswit return [objects[index]] return [] # handle others is more permissive - return [ - obj - for obj in objects - if obj.name.startswith(startswith) - and obj.name.endswith(endswith) - and obj.name.find(contains) >= 0 - ] - - -def find_objects_by_custom_property(objects, property_name="", property_value=""): - return [ - obj - for obj in objects - if property_name in obj and obj[property_name] == property_value - ] + return [obj for obj in objects if obj.name.startswith(startswith) and obj.name.endswith(endswith) and obj.name.find(contains) >= 0] +def find_objects_by_custom_property( + objects, + property_name="", + property_value=""): + return [obj for obj in objects if property_name in obj and obj[property_name] == property_value] def turn_collection_hierarchy_into_path(obj): parent_collection = obj.users_collection[0] - parent_names = [] + parent_names = [] parent_names.append(parent_collection.name) get_parent_collection_names(parent_collection, parent_names) parent_names.reverse() - return "\\".join(parent_names) - + return '\\'.join(parent_names) def find_font_object(fontcollection, font_name): - fonts = find_objects_by_custom_property(fontcollection.objects, "is_font", True) + fonts = find_objects_by_custom_property(fontcollection.objects, + "is_font", + True) for font in fonts: if font["font_name"] == font_name and font.parent == None: return font return None - def find_font_face_object(font_obj, face_name): - faces = find_objects_by_custom_property(font_obj.children, "is_face", True) + faces = find_objects_by_custom_property(font_obj.children, + "is_face", + True) for face in faces: if face["face_name"] == face_name: return face return None - def move_in_fontcollection(obj, fontcollection, allow_duplicates=False): # parent nesting structure # the font object - font_obj = find_font_object(fontcollection, obj["font_name"]) + font_obj = find_font_object(fontcollection, + obj["font_name"]) if font_obj == None: font_obj = bpy.data.objects.new(obj["font_name"], None) - font_obj.empty_display_type = "PLAIN_AXES" + font_obj.empty_display_type = 'PLAIN_AXES' fontcollection.objects.link(font_obj) # ensure custom properties are set @@ -385,10 +346,11 @@ def move_in_fontcollection(obj, fontcollection, allow_duplicates=False): font_obj["is_font"] = True # the face object as a child of font object - face_obj = find_font_face_object(font_obj, obj["face_name"]) + face_obj = find_font_face_object(font_obj, + obj["face_name"]) if face_obj == None: face_obj = bpy.data.objects.new(obj["face_name"], None) - face_obj.empty_display_type = "PLAIN_AXES" + face_obj.empty_display_type = 'PLAIN_AXES' face_obj["is_face"] = True fontcollection.objects.link(face_obj) @@ -403,11 +365,11 @@ def move_in_fontcollection(obj, fontcollection, allow_duplicates=False): glyphs_objs = find_objects_by_name(face_obj.children, startswith="glyphs") if len(glyphs_objs) <= 0: glyphs_obj = bpy.data.objects.new("glyphs", None) - glyphs_obj.empty_display_type = "PLAIN_AXES" + glyphs_obj.empty_display_type = 'PLAIN_AXES' fontcollection.objects.link(glyphs_obj) glyphs_obj.parent = face_obj elif len(glyphs_objs) > 1: - print("found more glyphs objects than expected") + print(f"found more glyphs objects than expected") # now it must exist glyphs_obj = find_objects_by_name(face_obj.children, startswith="glyphs")[0] @@ -415,11 +377,9 @@ def move_in_fontcollection(obj, fontcollection, allow_duplicates=False): glyphs_obj["font_name"] = obj["font_name"] def get_hash(o): - return hash(tuple(tuple(v.co) for v in o.data.vertices)) + return hash(tuple(tuple(v.co) for v in o.data.vertices )) - for other_obj in find_objects_by_custom_property( - glyphs_obj.children, "glyph", obj["glyph"] - ): + for other_obj in find_objects_by_custom_property(glyphs_obj.children, "glyph", obj["glyph"]): if get_hash(other_obj) == get_hash(obj) and not allow_duplicates: return other_obj @@ -434,50 +394,42 @@ def move_in_fontcollection(obj, fontcollection, allow_duplicates=False): return obj - def bpy_to_abspath(blender_path): return os.path.realpath(bpy.path.abspath(blender_path)) - def register_font_from_filepath(filepath): from .bimport import get_font_faces_in_file availables = get_font_faces_in_file(filepath) - + fonts = {} for a in availables: font_name = a["font_name"] face_name = a["face_name"] glyph = a["glyph"] - if font_name not in fonts: + if not font_name in fonts: fonts[font_name] = {} - if face_name not in fonts[font_name]: + if not face_name in fonts[font_name]: fonts[font_name][face_name] = [] fonts[font_name][face_name].append(glyph) for font_name in fonts: for face_name in fonts[font_name]: - Font.register_font( - font_name, face_name, fonts[font_name][face_name], filepath - ) - + Font.register_font(font_name, + face_name, + fonts[font_name][face_name], + filepath) def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""): if not filepath.endswith(".glb") and not filepath.endswith(".gltf"): - ShowMessageBox( - "Font loading error", - "ERROR", - f"Filepath({filepath}) is not a *.glb or *.gltf file", - ) + ShowMessageBox(f"Font loading error", 'ERROR', f"Filepath({filepath}) is not a *.glb or *.gltf file") return False marker_property = "font_import" - bpy.ops.abc3d.import_font_gltf( - filepath=filepath, - glyphs=glyphs, - marker_property=marker_property, - font_name=font_name, - face_name=face_name, - ) + bpy.ops.abc3d.import_font_gltf(filepath=filepath, + glyphs=glyphs, + marker_property=marker_property, + font_name=font_name, + face_name=face_name) fontcollection = bpy.data.collections.get("ABC3D") if fontcollection is None: @@ -485,7 +437,7 @@ def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""): modified_font_faces = [] all_glyph_os = [] - + all_objects = [] for o in bpy.context.scene.objects: if marker_property in o: if "type" in o and o["type"] == "glyph": @@ -496,39 +448,45 @@ def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""): font_name = o["font_name"] face_name = o["face_name"] - glyph_obj = move_in_fontcollection(o, fontcollection) + glyph_obj = move_in_fontcollection( + o, + fontcollection) glyph_obj_pointer = bpy.types.PointerProperty(glyph_obj) if glyph_obj == o: del o[marker_property] - Font.add_glyph(font_name, face_name, glyph_id, glyph_obj_pointer) + Font.add_glyph( + font_name, + face_name, + glyph_id, + glyph_obj_pointer) for c in o.children: if is_metrics_object(c): - add_metrics_obj_from_bound_box( - glyph_obj, bound_box_as_array(c.bound_box) - ) - modified_font_faces.append({"font_name": font_name, "face_name": face_name}) + add_metrics_obj_from_bound_box(glyph_obj, + bound_box_as_array(c.bound_box)) + modified_font_faces.append({"font_name": font_name, + "face_name": face_name}) for mff in modified_font_faces: - mff_glyphs = [] + glyphs = [] face = Font.fonts[mff["font_name"]].faces[mff["face_name"]] # iterate glyphs for g in face.glyphs: # iterate alternates for glyph in face.glyphs[g]: - mff_glyphs.append(get_original(glyph)) - if len(mff_glyphs) > 0: - add_default_metrics_to_objects(mff_glyphs) + glyphs.append(get_original(glyph)) + if len(glyphs) > 0: + add_default_metrics_to_objects(glyphs) # calculate unit factor - h = get_glyph_height(mff_glyphs[0]) + h = get_glyph_height(glyphs[0]) if h != 0: face.unit_factor = 1 / h update_available_fonts() remove_list = [] for o in bpy.context.scene.collection.all_objects: - if o.name not in fontcollection.all_objects: + if not o.name in fontcollection.all_objects: if marker_property in o and o[marker_property] == True: remove_list.append(o) @@ -536,7 +494,6 @@ def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""): # completely_delete_objects(remove_list) - def update_available_fonts(): abc3d_data = bpy.context.scene.abc3d_data @@ -552,30 +509,26 @@ def update_available_fonts(): f.face_name = face_name print(f"{__name__} added {font_name} {face_name}") - # def update_available_texts(): -# abc3d_data = bpy.context.scene.abc3d_data -# for o in bpy.context.scene.objects: -# if "linked_textobject" in o.keys(): -# i = o["linked_textobject"] -# found = False -# if len(abc3d_data.available_texts) > i: -# if abc3d_data.available_texts[i].glyphs - + # abc3d_data = bpy.context.scene.abc3d_data + # for o in bpy.context.scene.objects: + # if "linked_textobject" in o.keys(): + # i = o["linked_textobject"] + # found = False + # if len(abc3d_data.available_texts) > i: + # if abc3d_data.available_texts[i].glyphs def getPreferences(context): preferences = context.preferences - return preferences.addons["abc3d"].preferences - + return preferences.addons['abc3d'].preferences # clear available fonts def clear_available_fonts(): bpy.context.scene.abc3d_data.available_fonts.clear() - def load_installed_fonts(): preferences = getPreferences(bpy.context) - font_dir = os.path.join(preferences.assets_dir, "fonts") + font_dir = os.path.join(preferences.assets_dir,"fonts") if os.path.exists(font_dir): for file in os.listdir(font_dir): if file.endswith(".glb") or file.endswith(".gltf"): @@ -583,14 +536,13 @@ def load_installed_fonts(): # ShowMessageBox("Loading Font", "INFO", f"loading font from {font_path}") # print(f"loading font from {font_path}") # for f in bpy.context.scene.abc3d_data.available_fonts.values(): - # print(f"available font: {f.font_name} {f.face_name}") + # print(f"available font: {f.font_name} {f.face_name}") register_font_from_filepath(font_path) load_font_from_filepath(font_path) - def register_installed_fonts(): preferences = getPreferences(bpy.context) - font_dir = os.path.join(preferences.assets_dir, "fonts") + font_dir = os.path.join(preferences.assets_dir,"fonts") if os.path.exists(font_dir): for file in os.listdir(font_dir): if file.endswith(".glb") or file.endswith(".gltf"): @@ -598,14 +550,13 @@ def register_installed_fonts(): # ShowMessageBox("Loading Font", "INFO", f"loading font from {font_path}") # print(f"loading font from {font_path}") # for f in bpy.context.scene.abc3d_data.available_fonts.values(): - # print(f"available font: {f.font_name} {f.face_name}") + # print(f"available font: {f.font_name} {f.face_name}") register_font_from_filepath(font_path) - message_memory = [] +def ShowMessageBox(title = "Message Box", icon = 'INFO', message="", prevent_repeat=False): -def ShowMessageBox(title="Message Box", icon="INFO", message="", prevent_repeat=False): """Show a simple message box taken from `Link here `_ @@ -616,10 +567,10 @@ def ShowMessageBox(title="Message Box", icon="INFO", message="", prevent_repeat= :type icon: str :param message: lines of text to display, a.k.a. the message :type message: str or (str, str, ..) - + TIP: Check `Link blender icons `_ for icons you can use TIP: Or even better, check `Link this addons `_ to also see the icons. - + usage: .. code-block:: python myLines=("line 1","line 2","line 3") @@ -628,30 +579,27 @@ def ShowMessageBox(title="Message Box", icon="INFO", message="", prevent_repeat= or: .. code-block:: python butils.ShowMessageBox(title="",message=("AAAAAH","NOOOOO"),icon=) - + """ global message_memory if prevent_repeat: for m in message_memory: if m[0] == title and m[1] == icon and m[2] == message: + print("PREVENT PREVENT") return message_memory.append([title, icon, message]) - myLines = message - + myLines=message def draw(self, context): if isinstance(myLines, str): self.layout.label(text=myLines) elif hasattr(myLines, "__iter__"): for n in myLines: self.layout.label(text=n) - - bpy.context.window_manager.popup_menu(draw, title=title, icon=icon) - + bpy.context.window_manager.popup_menu(draw, title = title, icon = icon) def simply_delete_objects(objs): completely_delete_objects(objs) - def completely_delete_objects(objs, recursive=True): for g in objs: if type(g) != type(None): @@ -659,53 +607,43 @@ def completely_delete_objects(objs, recursive=True): try: if hasattr(g, "children") and len(g.children) > 0: completely_delete_objects(g.children) - except ReferenceError: + except ReferenceError as e: # not important pass try: bpy.data.objects.remove(g, do_unlink=True) - except ReferenceError: + except ReferenceError as e: # not important pass - def is_mesh(o): return type(o.data) == bpy.types.Mesh - def is_metrics_object(o): if f"{utils.prefix()}_type" in o: - return o[f"{utils.prefix()}_type"] == "metrics" - return ( - re.match(".*_metrics$", o.name) != None - or re.match(".*_metrics.[\d]{3}$", o.name) != None - ) and is_mesh(o) - + return o[f"{utils.prefix()}_type"] == 'metrics' + return (re.match(".*_metrics$", o.name) != None or re.match(".*_metrics.[\d]{3}$", o.name) != None) and is_mesh(o) def is_text_object(o): if f"{utils.prefix()}_type" in o: - return o[f"{utils.prefix()}_type"] == "textobject" + return o[f"{utils.prefix()}_type"] == 'textobject' for t in bpy.context.scene.abc3d_data.available_texts: if o == t.text_object: return True return False - def is_glyph(o): if f"{utils.prefix()}_type" in o: - return o[f"{utils.prefix()}_type"] == "glyph" + return o[f"{utils.prefix()}_type"] == 'glyph' try: - return ( - type(o.parent) is not type(None) - and "glyphs" in o.parent.name - and is_mesh(o) - and not is_metrics_object(o) - ) - except ReferenceError: + return type(o.parent) is not type(None) \ + and "glyphs" in o.parent.name \ + and is_mesh(o) \ + and not is_metrics_object(o) + except ReferenceError as e: return False - def update_types(): scene = bpy.context.scene abc3d_data = scene.abc3d_data @@ -714,42 +652,33 @@ def update_types(): for g in t.glyphs: g.glyph_object[f"{utils.prefix()}_type"] = "glyph" - # blender bound_box vertices -# -# 3------7. +# +# 3------7. # |`. | `. +y # | `2------6 | # | | | | | # 0---|--4. | +--- +x -# `. | `.| `. +# `. | `.| `. # `1------5 `+z - def get_glyph_advance(glyph_obj): for c in glyph_obj.children: if is_metrics_object(c): return abs(c.bound_box[4][0] - c.bound_box[0][0]) return abs(glyph_obj.bound_box[4][0] - glyph_obj.bound_box[0][0]) -def get_glyph_prepost_advances(glyph_obj): - for c in glyph_obj.children: - if is_metrics_object(c): - return -1 * c.bound_box[0][0], c.bound_box[4][0] - return -1 * glyph_obj.bound_box[0][0], glyph_obj.bound_box[4][0] - - def get_glyph_height(glyph_obj): for c in glyph_obj.children: if is_metrics_object(c): return abs(c.bound_box[0][1] - c.bound_box[3][1]) return abs(glyph_obj.bound_box[0][1] - glyph_obj.bound_box[3][1]) - def prepare_text(font_name, face_name, text, allow_replacement=True): loaded, missing, loadable, files = Font.test_glyphs_availability( - font_name, face_name, text - ) + font_name, + face_name, + text) # possibly replace upper and lower case letters with each other if len(missing) > 0 and allow_replacement: replacement_search = "" @@ -765,19 +694,17 @@ def prepare_text(font_name, face_name, text, allow_replacement=True): load_font_from_filepath(filepath, loadable, font_name, face_name) return True - def is_bezier(curve): - if curve.type != "CURVE": + if curve.type != 'CURVE': return False if len(curve.data.splines) < 1: return False for spline in curve.data.splines: - if spline.type != "BEZIER": + if spline.type != 'BEZIER': return False return True - -def would_regenerate(text_properties): +def will_regenerate(text_properties): mom = text_properties.text_object if len(text_properties.text) != len(text_properties.glyphs): @@ -786,7 +713,7 @@ def would_regenerate(text_properties): for i, g in enumerate(text_properties.glyphs): if not hasattr(g.glyph_object, "type"): return True - elif g.glyph_object.type != "EMPTY": + elif g.glyph_object.type != 'EMPTY': return True # check if perhaps one glyph was deleted elif type(g.glyph_object) == type(None): @@ -797,53 +724,14 @@ def would_regenerate(text_properties): return True elif len(text_properties.text) > i and g.glyph_id != text_properties.text[i]: return True - elif len(text_properties.text) > i and ( - g.glyph_object[f"{utils.prefix()}_font_name"] != text_properties.font_name - or g.glyph_object[f"{utils.prefix()}_face_name"] - != text_properties.face_name - ): + elif len(text_properties.text) > i and (g.glyph_object[f"{utils.prefix()}_font_name"] != text_properties.font_name + or g.glyph_object[f"{utils.prefix()}_face_name"] != text_properties.face_name): return True return False -def update_matrices(obj): - if obj.parent is None: - obj.matrix_world = obj.matrix_basis - - else: - obj.matrix_world = obj.parent.matrix_world * \ - obj.matrix_parent_inverse * \ - obj.matrix_basis - -def is_or_has_parent(o, parent, if_is_parent=True, max_depth=10): - if o == parent and if_is_parent: - return True - oo = o - for i in range(0, max_depth): - oo = oo.parent - if oo == parent: - return True - if oo is None: - return False - return False - -def parent_to_curve(o, c): - o.parent_type = 'OBJECT' - o.parent = c - # o.matrix_parent_inverse = c.matrix_world.inverted() - - if c.data.use_path and len(c.data.splines) > 0: - if c.data.splines[0].type == "BEZIER": - i = -1 if c.data.splines[0].use_cyclic_u else 0 - p = c.data.splines[0].bezier_points[i].co - o.matrix_parent_inverse.translation = p * -1.0 - elif c.data.splines[0].type == 'NURBS': - cm = c.to_mesh() - p = cm.vertices[0].co - o.matrix_parent_inverse.translation = p * -1.0 - -def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, can_regenerate=False): +def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4): """set_text_on_curve An earlier reset cancels the other. @@ -856,39 +744,199 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, :param reset_depsgraph_n: reset external parameters after n-th depsgraph update. (<= 0) = immediate, (> 0) = reset after n-th depsgraph update, (False) = no depsgraph reset :type reset_depsgraph_n: int """ - # NOTE: depsgraph update not locked - # as we fixed data_path with parent_to_curve trick - # global lock_depsgraph_update_n_times # starttime = time.perf_counter_ns() mom = text_properties.text_object if mom.type != "CURVE": return False - distribution_type = "CALCULATE" if is_bezier(mom) else "FOLLOW_PATH" + distribution_type = 'CALCULATE' if is_bezier(mom) else 'FOLLOW_PATH' - # NOTE: following not necessary anymore - # as we fixed data_path with parent_to_curve trick - # # use_path messes with parenting # however, we need it for follow_path # https://projects.blender.org/blender/blender/issues/100661 - # previous_use_path = mom.data.use_path - # if distribution_type == "CALCULATE": - # mom.data.use_path = False - # elif distribution_type == "FOLLOW_PATH": - # mom.data.use_path = True + previous_use_path = mom.data.use_path + if distribution_type == 'CALCULATE': + mom.data.use_path = False + elif distribution_type == 'FOLLOW_PATH': + mom.data.use_path = True - regenerate = can_regenerate and would_regenerate(text_properties) + regenerate = will_regenerate(text_properties) # if we regenerate.... delete objects if regenerate and text_properties.get("glyphs"): - for g in text_properties.glyphs: - print(dict(g)) - glyph_objects = [g["glyph_object"] for g in text_properties["glyphs"]] + glyph_objects = [ g["glyph_object"] for g in text_properties["glyphs"] ] completely_delete_objects(glyph_objects, True) text_properties.glyphs.clear() + curve_length = get_curve_length(mom) + advance = text_properties.offset + glyph_advance = 0 + is_command = False + previous_spline_index = -1 + for i, c in enumerate(text_properties.text): + face = Font.fonts[text_properties.font_name].faces[text_properties.face_name] + scalor = face.unit_factor * text_properties.font_size + if c == '\\': + is_command = True + continue + is_newline = False + if is_command: + if c == 'n': + is_newline = True + next_line_advance = get_next_line_advance(mom, advance, glyph_advance) + if advance == next_line_advance: + # self.report({'INFO'}, f"would like to add new line for {text_properties.text} please") + print(f"would like to add new line for {text_properties.text} please") + # TODO: add a new line + advance = next_line_advance + text_properties.offset + continue + is_command = False + glyph_id = c + + glyph_tmp = Font.get_glyph(text_properties.font_name, + text_properties.face_name, + glyph_id) + if glyph_tmp == None: + space_width = Font.is_space(glyph_id) + if space_width != False: + advance = advance + space_width * text_properties.font_size + continue + + message=f"Glyph not found for font_name='{text_properties.font_name}' face_name='{text_properties.face_name}' glyph_id='{glyph_id}'" + replaced = False + if glyph_id.isalpha(): + possible_replacement = glyph_id.swapcase() + glyph_tmp = Font.get_glyph(text_properties.font_name, + text_properties.face_name, + possible_replacement) + if glyph_tmp != None: + message = message + f" (replaced with '{possible_replacement}')" + replaced = True + + ShowMessageBox(title="Glyph replaced" if replaced else "Glyph missing", + icon='INFO' if replaced else 'ERROR', + message=message, + prevent_repeat=True) + if replaced == False: + continue + glyph = glyph_tmp.original + + ob = None + obg = None + if regenerate: + ob = bpy.data.objects.new(f"{glyph_id}", None) + obg = bpy.data.objects.new(f"{glyph_id}_mesh", glyph.data) + ob[f"{utils.prefix()}_type"] = "glyph" + ob[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id + ob[f"{utils.prefix()}_font_name"] = text_properties.font_name + ob[f"{utils.prefix()}_face_name"] = text_properties.face_name + else: + ob = text_properties.glyphs[i].glyph_object + for c in ob.children: + if c.name.startswith(f"{glyph_id}_mesh"): + obg = c + + if distribution_type == 'FOLLOW_PATH': + ob.constraints.new(type='FOLLOW_PATH') + ob.constraints["Follow Path"].target = mom + ob.constraints["Follow Path"].use_fixed_location = True + ob.constraints["Follow Path"].offset_factor = advance / curve_length + ob.constraints["Follow Path"].use_curve_follow = True + ob.constraints["Follow Path"].forward_axis = "FORWARD_X" + ob.constraints["Follow Path"].up_axis = "UP_Y" + spline_index = 0 + elif distribution_type == 'CALCULATE': + previous_ob_rotation_mode = None + previous_obg_rotation_mode = None + if ob.rotation_mode != 'QUATERNION': + ob.rotation_mode = 'QUATERNION' + previous_ob_rotation_mode = ob.rotation_mode + if obg.rotation_mode != 'QUATERNION': + obg.rotation_mode = 'QUATERNION' + previous_obg_rotation_mode = obg.rotation_mode + + location, tangent, spline_index = calc_point_on_bezier_curve(mom, advance, True, True) + if spline_index != previous_spline_index: + is_newline = True + + if regenerate: + ob.location = mom.matrix_world @ (location + text_properties.translation) + mom.users_collection[0].objects.link(obg) + mom.users_collection[0].objects.link(ob) + ob.parent = mom + obg.parent = ob + obg.location = mathutils.Vector((0.0, 0.0, 0.0)) + else: + ob.location = (location + text_properties.translation) + + if not text_properties.ignore_orientation: + mask = [0] + input_rotations = [mathutils.Vector((0.0, 0.0, 0.0))] + vectors = [tangent] + factors = [1.0] + local_main_axis = mathutils.Vector((1.0, 0.0, 0.0)) + motor = align_rotations_auto_pivot(mask, + input_rotations, + vectors, + factors, + local_main_axis) + + q = mathutils.Quaternion() + q.rotate(text_properties.orientation) + if regenerate: + obg.rotation_quaternion = q + ob.rotation_quaternion = (mom.matrix_world @ motor[0]).to_quaternion() + else: + ob.rotation_quaternion = motor[0].to_quaternion() + else: + q = mathutils.Quaternion() + q.rotate(text_properties.orientation) + # obg.rotation_quaternion = q + obg.rotation_quaternion = (mom.matrix_world @ q.to_matrix().to_4x4()).to_quaternion() + # ob.rotation_quaternion = (mom.matrix_world @ q.to_matrix().to_4x4()).to_quaternion() + + if previous_ob_rotation_mode: + ob.rotation_mode = previous_ob_rotation_mode + if previous_obg_rotation_mode: + obg.rotation_mode = previous_obg_rotation_mode + + glyph_advance = get_glyph_advance(glyph) * scalor + text_properties.letter_spacing + + # now we need to compensate for curvature + # otherwise letters will be closer together the curvier the bezier is + # this could be done more efficiently, but whatever + curve_compensation = 0 + if distribution_type == 'CALCULATE' and (not is_newline or spline_index == 0): # TODO: fix newline hack + if text_properties.compensate_curvature and glyph_advance > 0: + previous_location, psi = calc_point_on_bezier_curve(mom, advance, False, True) + new_location, si = calc_point_on_bezier_curve(mom, advance + glyph_advance, False, True) + if psi == si: + while (previous_location - new_location).length > glyph_advance and psi == si: + curve_compensation = curve_compensation - glyph_advance * 0.01 + new_location, si = calc_point_on_bezier_curve(mom, + advance + glyph_advance + curve_compensation, + output_tangent=False, + output_spline_index=True) + while (previous_location - new_location).length < glyph_advance and psi == si: + curve_compensation = curve_compensation + glyph_advance * 0.01 + new_location, si = calc_point_on_bezier_curve(mom, + advance + glyph_advance + curve_compensation, + output_tangent=False, + output_spline_index=True) + + ob.scale = (scalor, scalor, scalor) + + advance = advance + glyph_advance + curve_compensation + previous_spline_index = spline_index + + if regenerate: + glyph_data = text_properties.glyphs.add() + glyph_data.glyph_id = glyph_id + glyph_data.glyph_object = ob + glyph_data.letter_spacing = 0 + + if regenerate: mom[f"{utils.prefix()}_type"] = "textobject" mom[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id mom[f"{utils.prefix()}_font_name"] = text_properties.font_name @@ -898,295 +946,61 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, mom[f"{utils.prefix()}_orientation"] = text_properties.orientation mom[f"{utils.prefix()}_translation"] = text_properties.translation - curve_length = get_curve_length(mom) - advance = text_properties.offset - glyph_advance = 0 - glyph_index = 0 - is_command = False - previous_spline_index = -1 - - for i, c in enumerate(text_properties.text): - face = Font.fonts[text_properties.font_name].faces[text_properties.face_name] - scalor = face.unit_factor * text_properties.font_size - if c == "\\": - is_command = True - continue - is_newline = False - if is_command: - if c == "n": - is_newline = True - next_line_advance = get_next_line_advance(mom, advance, glyph_advance) - if advance == next_line_advance: - print( - f"would like to add new line for {text_properties.text} please" - ) - # TODO: add a new line - advance = next_line_advance + text_properties.offset - continue - is_command = False - glyph_id = c - - spline_index = 0 - - ############### GET GLYPH - - glyph_tmp = Font.get_glyph(text_properties.font_name, - text_properties.face_name, - glyph_id, - -1) - if glyph_tmp is None: - space_width = Font.is_space(glyph_id) - if space_width: - advance = advance + space_width * text_properties.font_size - continue - - message = f"Glyph not found for font_name='{text_properties.font_name}' face_name='{text_properties.face_name}' glyph_id='{glyph_id}'" - replaced = False - if glyph_id.isalpha(): - possible_replacement = glyph_id.swapcase() - glyph_tmp = Font.get_glyph( - text_properties.font_name, - text_properties.face_name, - possible_replacement, - -1 - ) - if glyph_tmp is not None: - message = message + f" (replaced with '{possible_replacement}')" - replaced = True - - if can_regenerate: - ShowMessageBox( - title="Glyph replaced" if replaced else "Glyph missing", - icon="INFO" if replaced else "ERROR", - message=message, - prevent_repeat=True, - ) - if not replaced: - continue - - glyph = glyph_tmp.original - - ############### GLYPH PROPERTIES - - glyph_properties = text_properties.glyphs[glyph_index] if not regenerate else text_properties.glyphs.add() - - if regenerate: - glyph_properties["glyph_id"] = glyph_id - glyph_properties["text_id"] = text_properties.text_id - glyph_properties["letter_spacing"] = 0 - - ############### NODE SCENE MANAGEMENT - - inner_node = None - outer_node = None - if regenerate: - outer_node = bpy.data.objects.new(f"{glyph_id}", None) - inner_node = bpy.data.objects.new(f"{glyph_id}_mesh", glyph.data) - outer_node[f"{utils.prefix()}_type"] = "glyph" - outer_node[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id - outer_node[f"{utils.prefix()}_glyph_index"] = glyph_index - outer_node[f"{utils.prefix()}_font_name"] = text_properties.font_name - outer_node[f"{utils.prefix()}_face_name"] = text_properties.face_name - - # Add into the scene. - mom.users_collection[0].objects.link(outer_node) - mom.users_collection[0].objects.link(inner_node) - # bpy.context.scene.collection.objects.link(inner_node) - - # Parenting is hard. - inner_node.parent_type = 'OBJECT' - inner_node.parent = outer_node - inner_node.matrix_parent_inverse = outer_node.matrix_world.inverted() - parent_to_curve(outer_node, mom) - outer_node.hide_set(True) - - glyph_properties["glyph_object"] = outer_node + if "lock_depsgraph_update_ntimes" in bpy.context.scene.abc3d_data: + bpy.context.scene.abc3d_data["lock_depsgraph_update_ntimes"] += len(bpy.context.selected_objects) else: - outer_node = glyph_properties.glyph_object - outer_node[f"{utils.prefix()}_glyph_index"] = glyph_index - for c in outer_node.children: - if c.name.startswith(f"{glyph_id}_mesh"): - inner_node = c + bpy.context.scene.abc3d_data["lock_depsgraph_update_ntimes"] = len(bpy.context.selected_objects) - ############### TRANSFORMS + # NOTE: we reset with a timeout, as setting and resetting certain things + # in fast succession will cause visual glitches (e.g. {}.data.use_path). + def reset(): + mom.data.use_path = previous_use_path + if counted_reset in bpy.app.handlers.depsgraph_update_post: + bpy.app.handlers.depsgraph_update_post.remove(counted_reset) + if bpy.app.timers.is_registered(reset): + bpy.app.timers.unregister(reset) - # origins could be shifted - # so we need to apply a pre_advance - glyph_pre_advance, glyph_post_advance = get_glyph_prepost_advances(glyph) - advance += glyph_pre_advance * scalor + molotov = reset_depsgraph_n + 0 + def counted_reset(scene, depsgraph): + nonlocal molotov + if molotov == 0: + reset() + else: + molotov -= 1 - # check if we want to loop - applied_advance = advance - if text_properties.loop_in: - if applied_advance < 0: - applied_advance %= curve_length + # unregister previous resets to avoid multiple execution + if bpy.app.timers.is_registered(reset): + bpy.app.timers.unregister(reset) + if counted_reset in bpy.app.handlers.depsgraph_update_post: + bpy.app.handlers.depsgraph_update_post.remove(counted_reset) - if text_properties.loop_out: - if applied_advance > curve_length: - applied_advance %= curve_length + if not isinstance(reset_timeout_s, bool): + if reset_timeout_s > 0: + bpy.app.timers.register(reset, first_interval=reset_timeout_s) + elif reset_timeout <= 0: + reset() - if distribution_type == "FOLLOW_PATH": - outer_node.constraints.new(type="FOLLOW_PATH") - outer_node.constraints["Follow Path"].target = mom - outer_node.constraints["Follow Path"].use_fixed_location = True - outer_node.constraints["Follow Path"].offset_factor = applied_advance / curve_length - outer_node.constraints["Follow Path"].use_curve_follow = True - outer_node.constraints["Follow Path"].forward_axis = "FORWARD_X" - outer_node.constraints["Follow Path"].up_axis = "UP_Y" - spline_index = 0 - elif distribution_type == "CALCULATE": - previous_outer_node_rotation_mode = None - previous_inner_node_rotation_mode = None - if outer_node.rotation_mode != "QUATERNION": - outer_node.rotation_mode = "QUATERNION" - previous_outer_node_rotation_mode = outer_node.rotation_mode - if inner_node.rotation_mode != "QUATERNION": - inner_node.rotation_mode = "QUATERNION" - previous_inner_node_rotation_mode = inner_node.rotation_mode - - # get info from bezier - location, tangent, spline_index = calc_point_on_bezier_curve(mom, applied_advance, True, True) - - # check if we are on a new line - if spline_index != previous_spline_index: - is_newline = True - - # position - outer_node.location = location + text_properties.translation - - # orientation / rotation - mask = [0] - input_rotations = [mathutils.Vector((0.0, 0.0, 0.0))] - vectors = [tangent] - factors = [1.0] - local_main_axis = mathutils.Vector((1.0, 0.0, 0.0)) - motor = align_rotations_auto_pivot( - mask, input_rotations, vectors, factors, local_main_axis - ) if not text_properties.ignore_orientation else [mathutils.Matrix()] - - q = mathutils.Quaternion() - q.rotate(text_properties.orientation) - outer_node.rotation_quaternion = (motor[0].to_3x3() @ q.to_matrix()).to_quaternion() - - # # NOTE: supercool but out of scope, as we wouldhave to update it everytime the curve object rotates, - # # but this would ignore the curve objects orientation: - # outer_node.rotation_quaternion = (mom.matrix_world.inverted().to_3x3() @ motor[0].to_3x3() @ q.to_matrix()).to_quaternion() - - # # scale - outer_node.scale = (scalor, scalor, scalor) - - if previous_outer_node_rotation_mode: - outer_node.rotation_mode = previous_outer_node_rotation_mode - if previous_inner_node_rotation_mode: - inner_node.rotation_mode = previous_inner_node_rotation_mode - - # outer_node.hide_viewport = True - - ############### PREPARE FOR THE NEXT - - glyph_advance = ( - glyph_post_advance * scalor + text_properties.letter_spacing + glyph_properties.letter_spacing - ) - - # now we need to compensate for curvature - # otherwise letters will be closer together the curvier the bezier is - # NOTE: this could be done more efficiently - curve_compensation = 0 - if distribution_type == "CALCULATE" and ( - not is_newline or spline_index == 0 - ): - if text_properties.compensate_curvature and glyph_advance > 0: - previous_location, psi = calc_point_on_bezier_curve( - mom, advance, False, True - ) - new_location, si = calc_point_on_bezier_curve( - mom, advance + glyph_advance, False, True - ) - if psi == si: - while ( - previous_location - new_location - ).length > glyph_advance and psi == si: - curve_compensation = curve_compensation - glyph_advance * 0.01 - new_location, si = calc_point_on_bezier_curve( - mom, - advance + glyph_advance + curve_compensation, - output_tangent=False, - output_spline_index=True, - ) - while ( - previous_location - new_location - ).length < glyph_advance and psi == si: - curve_compensation = curve_compensation + glyph_advance * 0.01 - new_location, si = calc_point_on_bezier_curve( - mom, - advance + glyph_advance + curve_compensation, - output_tangent=False, - output_spline_index=True, - ) - - advance = advance + glyph_advance + curve_compensation - glyph_index += 1 - previous_spline_index = spline_index - - # NOTE: depsgraph update not locked - # as we fixed data_path with parent_to_curve trick - # if lock_depsgraph_update_n_times < 0: - # lock_depsgraph_update_n_times = len( - # bpy.context.selected_objects - # ) - # else: - # lock_depsgraph_update_n_times += len( - # bpy.context.selected_objects - # ) - # # NOTE: we reset with a timeout, as setting and resetting certain things - # # in fast succession will cause visual glitches (e.g. {}.data.use_path). - # def reset(): - # mom.data.use_path = previous_use_path - # if counted_reset in bpy.app.handlers.depsgraph_update_post: - # bpy.app.handlers.depsgraph_update_post.remove(counted_reset) - # if bpy.app.timers.is_registered(reset): - # bpy.app.timers.unregister(reset) - # molotov = reset_depsgraph_n + 0 - # def counted_reset(scene, depsgraph): - # nonlocal molotov - # if molotov == 0: - # reset() - # else: - # molotov -= 1 - # # unregister previous resets to avoid multiple execution - # if bpy.app.timers.is_registered(reset): - # bpy.app.timers.unregister(reset) - # if counted_reset in bpy.app.handlers.depsgraph_update_post: - # bpy.app.handlers.depsgraph_update_post.remove(counted_reset) - # if not isinstance(reset_timeout_s, bool): - # if reset_timeout_s > 0: - # bpy.app.timers.register(reset, first_interval=reset_timeout_s) - # elif reset_timeout <= 0: - # reset() - # bpy.app.handlers.depsgraph_update_post.append(counted_reset) + bpy.app.handlers.depsgraph_update_post.append(counted_reset) # endtime = time.perf_counter_ns() # elapsedtime = endtime - starttime return True - verification_object = { - f"{utils.prefix()}_type": "textobject", - f"{utils.prefix()}_linked_textobject": 0, - f"{utils.prefix()}_font_name": "font_name", - f"{utils.prefix()}_face_name": "face_name", - f"{utils.prefix()}_font_size": 42, - f"{utils.prefix()}_letter_spacing": 42, - f"{utils.prefix()}_orientation": [0, 0, 0], - f"{utils.prefix()}_translation": [0, 0, 0], -} - + f"{utils.prefix()}_type": "textobject", + f"{utils.prefix()}_linked_textobject": 0, + f"{utils.prefix()}_font_name": "font_name", + f"{utils.prefix()}_face_name": "face_name", + f"{utils.prefix()}_font_size": 42, + f"{utils.prefix()}_letter_spacing": 42, + f"{utils.prefix()}_orientation": [0,0,0], + f"{utils.prefix()}_translation": [0,0,0], + } def verify_text_object(o): pass - def transfer_text_properties_to_text_object(text_properties, o): o[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id o[f"{utils.prefix()}_font_name"] = text_properties.font_name @@ -1197,7 +1011,6 @@ def transfer_text_properties_to_text_object(text_properties, o): o[f"{utils.prefix()}_translation"] = text_properties.translation o[f"{utils.prefix()}_text"] = text_properties["text"] - def transfer_text_object_to_text_properties(o, text_properties): text_properties["text_id"] = o[f"{utils.prefix()}_linked_textobject"] text_properties["font_name"] = o[f"{utils.prefix()}_font_name"] @@ -1208,17 +1021,15 @@ def transfer_text_object_to_text_properties(o, text_properties): text_properties["translation"] = o[f"{utils.prefix()}_translation"] text_properties["text"] = o[f"{utils.prefix()}_text"] - # blender bound_box vertices -# -# 3------7. +# +# 3------7. # |`. | `. +y # | `2------6 -z | # | | | | `. | # 0---|--4. | `+--- +x -# `. | `.| -# `1------5 - +# `. | `.| +# `1------5 def add_metrics_obj_from_bound_box(glyph, bound_box=None): mesh = bpy.data.meshes.new(f"{glyph.name}_metrics") # add the new mesh @@ -1243,78 +1054,49 @@ def add_metrics_obj_from_bound_box(glyph, bound_box=None): if type(bound_box) == type(None): bound_box = glyph.bound_box - - verts = [ - bound_box[0], - bound_box[1], - bound_box[2], - bound_box[3], - bound_box[4], - bound_box[5], - bound_box[6], - bound_box[7], - ] - edges = [ - [0, 1], - [1, 2], - [2, 3], - [3, 0], - [0, 4], - [1, 5], - [2, 6], - [3, 7], - [4, 5], - [5, 6], - [6, 7], - [7, 4], - ] + + verts = [bound_box[0], + bound_box[1], + bound_box[2], + bound_box[3], + bound_box[4], + bound_box[5], + bound_box[6], + bound_box[7], + ] + edges = [[0,1],[1,2],[2,3],[3,0], + [0,4],[1,5],[2,6],[3,7], + [4,5],[5,6],[6,7],[7,4], + ] faces = [] - + mesh.from_pydata(verts, edges, faces) - def add_faces_to_metrics(obj): mesh = bpy.data.meshes.new(f"{obj.name}") # add the new mesh print(f"add_faces_to_metrics for {obj.name}") bound_box = bound_box_as_array(obj.bound_box) - verts = [ - bound_box[0], - bound_box[1], - bound_box[2], - bound_box[3], - bound_box[4], - bound_box[5], - bound_box[6], - bound_box[7], - ] - edges = [ - [0, 1], - [1, 2], - [2, 3], - [3, 0], - [0, 4], - [1, 5], - [2, 6], - [3, 7], - [4, 5], - [5, 6], - [6, 7], - [7, 4], - ] + verts = [bound_box[0], + bound_box[1], + bound_box[2], + bound_box[3], + bound_box[4], + bound_box[5], + bound_box[6], + bound_box[7], + ] + edges = [[0,1],[1,2],[2,3],[3,0], + [0,4],[1,5],[2,6],[3,7], + [4,5],[5,6],[6,7],[7,4], + ] faces = [ - [0, 1, 2], - [2, 3, 0], - [2, 6, 7], - [7, 3, 2], - [6, 5, 4], - [4, 7, 6], - [4, 5, 1], - [0, 4, 1], - [1, 5, 6], - [1, 6, 2], - [4, 0, 7], - [7, 0, 3], - ] + [0,1,2], [2,3,0], + [2,6,7], [7,3,2], + [6,5,4], [4,7,6], + [4,5,1], [0,4,1], + [1,5,6], [1,6,2], + [4,0,7], [7,0,3], + ] mesh.from_pydata(verts, edges, faces) @@ -1322,35 +1104,24 @@ def add_faces_to_metrics(obj): obj.data = mesh bpy.data.meshes.remove(old_mesh) - def remove_faces_from_metrics(obj): mesh = bpy.data.meshes.new(f"{obj.name}") # add the new mesh bound_box = bound_box_as_array(obj.bound_box) - verts = [ - bound_box[0], - bound_box[1], - bound_box[2], - bound_box[3], - bound_box[4], - bound_box[5], - bound_box[6], - bound_box[7], - ] - edges = [ - [0, 1], - [1, 2], - [2, 3], - [3, 0], - [0, 4], - [1, 5], - [2, 6], - [3, 7], - [4, 5], - [5, 6], - [6, 7], - [7, 4], - ] - faces = [] + verts = [bound_box[0], + bound_box[1], + bound_box[2], + bound_box[3], + bound_box[4], + bound_box[5], + bound_box[6], + bound_box[7], + ] + edges = [[0,1],[1,2],[2,3],[3,0], + [0,4],[1,5],[2,6],[3,7], + [4,5],[5,6],[6,7],[7,4], + ] + faces = [ + ] mesh.from_pydata(verts, edges, faces) @@ -1358,63 +1129,59 @@ def remove_faces_from_metrics(obj): obj.data = mesh bpy.data.meshes.remove(old_mesh) - # duplicate # def remove_metrics_from_selection(): -# for o in bpy.context.selected_objects: -# is_possibly_glyph = is_mesh(o) -# if is_possibly_glyph: -# metrics = [] -# for c in o.children: -# if is_metrics_object(c): -# metrics.append(c) -# completely_delete_objects(metrics) + # for o in bpy.context.selected_objects: + # is_possibly_glyph = is_mesh(o) + # if is_possibly_glyph: + # metrics = [] + # for c in o.children: + # if is_metrics_object(c): + # metrics.append(c) + # completely_delete_objects(metrics) - -def get_max_bound_box(bb_1, bb_2=None): +def get_max_bound_box(bb_1, bb_2 = None): if type(bb_2) == type(None): bb_2 = bb_1 x_max = max(bb_1[4][0], bb_2[4][0]) x_min = min(bb_1[0][0], bb_2[0][0]) - y_max = max(bb_1[3][1], bb_2[3][1]) - y_min = min(bb_1[0][1], bb_2[0][1]) + y_max = max(bb_1[3][1], bb_2[3][1]) + y_min = min(bb_1[0][1], bb_2[0][1]) z_max = max(bb_1[1][2], bb_2[1][2]) z_min = min(bb_1[0][2], bb_2[0][2]) return [ - mathutils.Vector((x_min, y_min, z_min)), - mathutils.Vector((x_min, y_min, z_max)), - mathutils.Vector((x_min, y_max, z_max)), - mathutils.Vector((x_min, y_max, z_min)), - mathutils.Vector((x_max, y_min, z_min)), - mathutils.Vector((x_max, y_min, z_max)), - mathutils.Vector((x_max, y_max, z_max)), - mathutils.Vector((x_max, y_max, z_min)), - ] + mathutils.Vector((x_min, y_min, z_min)), + mathutils.Vector((x_min, y_min, z_max)), + mathutils.Vector((x_min, y_max, z_max)), + mathutils.Vector((x_min, y_max, z_min)), + mathutils.Vector((x_max, y_min, z_min)), + mathutils.Vector((x_max, y_min, z_max)), + mathutils.Vector((x_max, y_max, z_max)), + mathutils.Vector((x_max, y_max, z_min)), + ] # blender bound_box vertices -# -# 3------7. +# +# 3------7. # |`. | `. +y # | `2------6 | # | | | | | # 0---|--4. | +--- +x -# `. | `.| `. +# `. | `.| `. # `1------5 `+z - # why not [ [0] * 3 ] * 8 # https://stackoverflow.com/questions/2397141/how-to-initialize-a-two-dimensional-array-list-of-lists-if-not-using-numpy-in def bound_box_as_array(bound_box): - array = [[0] * 3 for i in range(8)] + array = [ [0] * 3 for i in range(8) ] for i in range(0, len(bound_box)): for j in range(0, len(bound_box[i])): array[i][j] = bound_box[i][j] return array - ## -# @brief get_metrics_bound_box +# @brief get_metrics_bound_box # generates a metrics bounding box # where x-width comes from bb # and y-height + z-depth from bb_uebermetrics @@ -1440,7 +1207,6 @@ def get_metrics_bound_box(bb, bb_uebermetrics): metrics[7][0] = bb[7][0] return metrics - def get_metrics_object(o): if is_glyph(o): for c in o.children: @@ -1448,17 +1214,15 @@ def get_metrics_object(o): return c return None - def get_original(o): if hasattr(o, "original"): return o.original else: return o - def add_default_metrics_to_objects(objects=None, overwrite_existing=False): if type(objects) == type(None): - objects = bpy.context.selected_objects + objects=bpy.context.selected_objects targets = [] reference_bound_box = None for o in objects: @@ -1471,28 +1235,21 @@ def add_default_metrics_to_objects(objects=None, overwrite_existing=False): if len(metrics) == 0: targets.append(o) - reference_bound_box = get_max_bound_box( - o.bound_box, reference_bound_box - ) + reference_bound_box = get_max_bound_box(o.bound_box, reference_bound_box) elif len(metrics) >= 0 and overwrite_existing: completely_delete_objects(metrics) targets.append(o) - reference_bound_box = get_max_bound_box( - o.bound_box, reference_bound_box - ) + reference_bound_box = get_max_bound_box(o.bound_box, reference_bound_box) else: for m in metrics: - reference_bound_box = get_max_bound_box( - m.bound_box, reference_bound_box - ) + reference_bound_box = get_max_bound_box(m.bound_box, reference_bound_box) for t in targets: bound_box = get_metrics_bound_box(t.bound_box, reference_bound_box) add_metrics_obj_from_bound_box(t, bound_box) - def remove_metrics_from_objects(objects=None): if type(objects) == type(None): - objects = bpy.context.selected_objects + objects=bpy.context.selected_objects metrics = [] for o in objects: for c in o.children: @@ -1500,10 +1257,9 @@ def remove_metrics_from_objects(objects=None): metrics.append(c) completely_delete_objects(metrics) - def align_metrics_of_objects_to_active_object(objects=None): if type(objects) == type(None): - objects = bpy.context.selected_objects + objects=bpy.context.selected_objects if len(objects) == 0: return "no objects selected" @@ -1523,7 +1279,7 @@ def align_metrics_of_objects_to_active_object(objects=None): # do it for o in objects: is_possibly_glyph = is_glyph(o) - if is_possibly_glyph and o is not bpy.context.active_object: + if is_possibly_glyph: metrics = [] for c in o.children: if is_metrics_object(c): @@ -1531,19 +1287,20 @@ def align_metrics_of_objects_to_active_object(objects=None): bb = None if len(metrics) == 0: - bb = get_metrics_bound_box(o.bound_box, reference_bound_box) + bb = get_metrics_bound_box(o.bound_box, + reference_bound_box) else: - bb = get_metrics_bound_box(metrics[0].bound_box, reference_bound_box) + bb = get_metrics_bound_box(metrics[0].bound_box, + reference_bound_box) if len(metrics) > 0: completely_delete_objects(metrics) - + add_metrics_obj_from_bound_box(o, bb) return "" - def align_metrics_of_objects(objects=None): if type(objects) == type(None): - objects = bpy.context.selected_objects + objects=bpy.context.selected_objects if len(objects) == 0: return "no objects selected" @@ -1558,13 +1315,11 @@ def align_metrics_of_objects(objects=None): metrics.append(c) if len(metrics) == 0: - reference_bound_box = get_max_bound_box( - o.bound_box, reference_bound_box - ) + reference_bound_box = get_max_bound_box(o.bound_box, + reference_bound_box) elif len(metrics) > 0: - reference_bound_box = get_max_bound_box( - metrics[0].bound_box, reference_bound_box - ) + reference_bound_box = get_max_bound_box(metrics[0].bound_box, + reference_bound_box) targets.append(o) for t in targets: metrics = [] @@ -1573,116 +1328,13 @@ def align_metrics_of_objects(objects=None): metrics.append(c) bound_box = None if len(metrics) == 0: - bound_box = get_metrics_bound_box(t.bound_box, reference_bound_box) + bound_box = get_metrics_bound_box(t.bound_box, + reference_bound_box) else: - bound_box = get_metrics_bound_box(metrics[0].bound_box, reference_bound_box) + bound_box = get_metrics_bound_box(metrics[0].bound_box, + reference_bound_box) completely_delete_objects(metrics) - + add_metrics_obj_from_bound_box(t, bound_box) return "" -def align_origins_to_active_object(objects=None, axis=2): - if objects is None: - objects = bpy.context.selected_objects - if len(objects) == 0: - return "no objects selected" - - if bpy.context.active_object is None: - return "no active object selected" - - reference_origin_position = bpy.context.active_object.matrix_world.translation[axis] - - # do it - for o in objects: - is_possibly_glyph = is_glyph(o) - if is_possibly_glyph and o is not bpy.context.active_object: - if is_mesh(o): - diff = reference_origin_position - o.matrix_world.translation[axis] - - for v in o.data.vertices: - v.co[axis] -= diff - - o.matrix_world.translation[axis] = reference_origin_position - - return "" - -# NOTE: -# Following code is not necessary anymore, -# as we derive the advance through metrics -# boundaries - -# def divide_vectors(v1=mathutils.Vector((1.0,1.0,1.0)), v2=mathutils.Vector((1.0,1.0,1.0))): - # return mathutils.Vector([v1[i] / v2[i] for i in range(3)]) - -# def get_origin_shift_metrics(o, axis=0): - # if not is_metrics_object(o): - # return False - # min_value = sys.float_info.max - # for v in o.data.vertices: - # if v.co[axis] < min_value: - # min_value = v.co[axis] - # if min_value == sys.float_info.max: - # return False - # return min_value - -# def fix_origin_shift_metrics(o, axis=0): - # shift = get_origin_shift_metrics(o) - # if not shift: - # print("False") - # return False - # for v in o.data.vertices: - # v.co[axis] -= shift - # shift_vector = mathutils.Vector((0.0, 0.0, 0.0)) - # shift_vector[axis] = shift - # # o.location = o.location - (divide_vectors(v2=o.matrix_world.to_scale()) * (o.matrix_world @ shift_vector)) - # o.matrix_local.translation = o.matrix_local.translation + (shift_vector @ o.matrix_local.inverted()) - # # update_matrices(o) - # return True - - -# def fix_objects_metrics_origins(objects=None, axis=0, handle_metrics_directly=True): - # if objects is None: - # objects = bpy.context.selected_objects - # if len(objects) == 0: - # return "no objects selected" - - # for o in objects: - # is_possibly_glyph = is_glyph(o) - # if is_possibly_glyph: - # for c in o.children: - # if is_metrics_object(c): - # fix_origin_shift_metrics(c, axis) - # elif is_metrics_object(o) and handle_metrics_directly: - # fix_origin_shift_metrics(o, axis) - # return "" - -# def align_origins_to_metrics(objects=None): - # if objects is None: - # objects = bpy.context.selected_objects - # if len(objects) == 0: - # return "no objects selected" - - # for o in objects: - # is_possibly_glyph = is_glyph(o) - # if is_possibly_glyph: - # min_x = 9999999999 - # for c in o.children: - # if is_metrics_object(c): - # for v in c.data.vertices: - # if v.co[0] < min_x: - # min_x = v.co[0] - - # metrics_origin_x = c.matrix_world.translation[0] + min_x - - # diff = metrics_origin_x - o.matrix_world.translation[0] - - # for v in o.data.vertices: - # v.co[0] -= diff - - # o.location += mathutils.Vector((diff, 0.0, 0.0)) @ o.matrix_world.inverted() - - # for c in o.children: - # if is_metrics_object(c): - # c.location -= mathutils.Vector((diff, 0.0, 0.0)) @ o.matrix_world.inverted() - - # return "" diff --git a/common/Font.py b/common/Font.py index bcb7949..1c8368e 100644 --- a/common/Font.py +++ b/common/Font.py @@ -1,68 +1,70 @@ +from typing import TypedDict from typing import Dict +from dataclasses import dataclass from pathlib import Path # convenience dictionary for translating names to glyph ids # note: overwritten/extended by the content of "glypNamesToUnicode.txt" # when addon is registered in __init__.py name_to_glyph_d = { - "zero": "0", - "one": "1", - "two": "2", - "three": "3", - "four": "4", - "five": "5", - "six": "6", - "seven": "7", - "eight": "8", - "nine": "9", - "ampersand": "&", - "backslash": "\\", - "colon": ":", - "comma": ",", - "equal": "=", - "exclam": "!", - "hyphen": "-", - "minus": "−", - "parenleft": "(", - "parenright": "(", - "period": ".", - "plus": "+", - "question": "?", - "quotedblleft": "“", - "quotedblright": "”", - "semicolon": ";", - "slash": "/", - "space": " ", -} + "zero": "0", + "one": "1", + "two": "2", + "three": "3", + "four": "4", + "five": "5", + "six": "6", + "seven": "7", + "eight": "8", + "nine": "9", + "ampersand": "&", + "backslash": "\\", + "colon": ":", + "comma": ",", + "equal": "=", + "exclam": "!", + "hyphen": "-", + "minus": "−", + "parenleft": "(", + "parenright": "(", + "period": ".", + "plus": "+", + "question": "?", + "quotedblleft": "“", + "quotedblright": "”", + "semicolon": ";", + "slash": "/", + "space": " ", + } space_d = {} known_misspellings = { - # simple misspelling - "excent": "accent", - "overdot": "dotaccent", - "diaresis": "dieresis", - "diaeresis": "dieresis", - # different conventions - "doubleacute": "hungarumlaut", - # character does not exist.. maybe something else - "Wcaron": "Wcircumflex", - "Neng": "Nlongrightleg", - "Lgrave": "Lacute", - # currency stuff - "doller": "dollar", - "euro": "Euro", - "yuan": "yen", # https://en.wikipedia.org/wiki/Yen_and_yuan_sign - "pound": "sterling", - # whoopsie - "__": "_", -} - + # simple misspelling + "excent" : "accent", + "overdot" : "dotaccent", + "diaresis": "dieresis", + "diaeresis": "dieresis", + # character does not exist.. maybe something else + "Odoubleacute": "Ohungarumlaut", + "Udoubleacute": "Uhungarumlaut", + "Wcaron": "Wcircumflex", + "Neng": "Nlongrightleg", + "Lgrave": "Lacute", + # currency stuff + "doller": "dollar", + "euro": "Euro", + "yuan": "yen", # https://en.wikipedia.org/wiki/Yen_and_yuan_sign + "pound": "sterling", + # whoopsie + "__": "_", + } def fix_glyph_name_misspellings(name): for misspelling in known_misspellings: if misspelling in name: - return name.replace(misspelling, known_misspellings[misspelling]) + return name.replace(misspelling, + known_misspellings[misspelling]) return name @@ -75,13 +77,6 @@ def name_to_glyph(name): return None -def glyph_to_name(glyph_id): - for k in name_to_glyph_d: - if glyph_id == name_to_glyph_d[k]: - return k - return glyph_id - - def is_space(character): for name in space_d: if character == space_d[name][0]: @@ -93,37 +88,33 @@ def generate_from_file_d(filepath): d = {} with open(filepath) as f: for line in f: - if line[0] == "#": + if line[0] == '#': continue - split = line.split(" ") + split = line.split(' ') if len(split) == 2: - (name, hexstr) = line.split(" ") + (name, hexstr) = line.split(' ') val = chr(int(hexstr, base=16)) d[name] = val if len(split) == 3: # we might have a parameter, like for the spaces - (name, hexstr, parameter) = line.split(" ") + (name, hexstr, parameter) = line.split(' ') parameter_value = float(parameter) val = chr(int(hexstr, base=16)) d[name] = [val, parameter_value] return d - def generate_name_to_glyph_d(): return generate_from_file_d(f"{Path(__file__).parent}/glyphNamesToUnicode.txt") - def generate_space_d(): return generate_from_file_d(f"{Path(__file__).parent}/spacesUnicode.txt") - def init(): global name_to_glyph_d global space_d name_to_glyph_d = generate_name_to_glyph_d() space_d = generate_space_d() - class FontFace: """FontFace is a class holding glyphs @@ -136,8 +127,8 @@ class FontFace: :param filenames: from which file is this face :type filenames: List[str] """ - - def __init__(self, glyphs={}): + def __init__(self, + glyphs = {}): self.glyphs = glyphs # lists have to be initialized in __init__ # to be attributes per instance. @@ -148,15 +139,13 @@ class FontFace: self.filepaths = [] self.unit_factor = 1.0 - class Font: """Font holds the faces and various metadata for a font :param faces: dictionary of faces, defaults to ``Dict[str, FontFace]`` :type faces: Dict[str, FontFace] """ - - def __init__(self, faces=Dict[str, FontFace]): + def __init__(self, faces = Dict[str, FontFace]): self.faces = faces @@ -167,18 +156,14 @@ def register_font(font_name, face_name, glyphs_in_fontfile, filepath): fonts[font_name].faces[face_name] = FontFace({}) fonts[font_name].faces[face_name].glyphs_in_fontfile = glyphs_in_fontfile else: - fonts[font_name].faces[face_name].glyphs_in_fontfile = list( - set( - fonts[font_name].faces[face_name].glyphs_in_fontfile - + glyphs_in_fontfile - ) - ) + fonts[font_name].faces[face_name].glyphs_in_fontfile = \ + list(set(fonts[font_name].faces[face_name].glyphs_in_fontfile + glyphs_in_fontfile)) if filepath not in fonts[font_name].faces[face_name].filepaths: fonts[font_name].faces[face_name].filepaths.append(filepath) - + def add_glyph(font_name, face_name, glyph_id, glyph_object): - """add_glyph adds a glyph to a FontFace + """ add_glyph adds a glyph to a FontFace it creates the :class:`Font` and :class:`FontFace` if it does not exist yet :param font_name: The Font you want to add the glyph to @@ -202,9 +187,8 @@ def add_glyph(font_name, face_name, glyph_id, glyph_object): if glyph_id not in fonts[font_name].faces[face_name].loaded_glyphs: fonts[font_name].faces[face_name].loaded_glyphs.append(glyph_id) - def get_glyph(font_name, face_name, glyph_id, alternate=0): - """add_glyph adds a glyph to a FontFace + """ add_glyph adds a glyph to a FontFace it creates the :class:`Font` and :class:`FontFace` if it does not exist yet :param font_name: The :class:`Font` you want to get the glyph from @@ -235,17 +219,14 @@ def get_glyph(font_name, face_name, glyph_id, alternate=0): if glyph_id not in fonts[font_name].faces[face_name].missing_glyphs: fonts[font_name].faces[face_name].missing_glyphs.append(glyph_id) return None - + return fonts[font_name].faces[face_name].glyphs.get(glyph_id)[alternate] - def test_glyphs_availability(font_name, face_name, text): # maybe there is NOTHING yet - if ( - not fonts.keys().__contains__(font_name) - or fonts[font_name].faces.get(face_name) == None - ): - return "", "", text # , , + if not fonts.keys().__contains__(font_name) or \ + fonts[font_name].faces.get(face_name) == None: + return "", "", text # , , loaded = [] missing = [] @@ -259,45 +240,36 @@ def test_glyphs_availability(font_name, face_name, text): if c not in fonts[font_name].faces[face_name].missing_glyphs: fonts[font_name].faces[face_name].missing_glyphs.append(c) missing.append(c) - return ( - "".join(loaded), - "".join(missing), - "".join(maybe), - fonts[font_name].faces[face_name].filepaths, - ) - + return ''.join(loaded), ''.join(missing), ''.join(maybe), fonts[font_name].faces[face_name].filepaths def get_loaded_fonts(): return fonts.keys() - def get_loaded_fonts_and_faces(): out = [] for f in fonts.keys(): for ff in fonts[f].faces.keys(): - out.append([f, ff]) + out.append([f,ff]) return out - MISSING_FONT = 0 MISSING_FACE = 1 - def test_availability(font_name, face_name, text): if not fonts.keys().__contains__(font_name): return MISSING_FONT if fonts[font_name].faces.get(face_name) == None: return MISSING_FACE - loaded, missing, maybe, filepaths = test_glyphs_availability( - font_name, face_name, text - ) + loaded, missing, maybe, filepaths = test_glyphs_availability(font_name, + face_name, + text) return { - "loaded": loaded, - "missing": missing, - "maybe": maybe, - "filepaths": filepaths, - } - + "loaded": loaded, + "missing": missing, + "maybe": maybe, + "filepaths": filepaths, + } + # holds all fonts fonts = {} diff --git a/common/utils.py b/common/utils.py index 8219a94..5a23e78 100644 --- a/common/utils.py +++ b/common/utils.py @@ -1,32 +1,25 @@ # NOTE: also change version in ../__init__.py def get_version_major(): return 0 - - def get_version_minor(): return 0 - - def get_version_patch(): - return 7 - - + return 4 def get_version_string(): return f"{get_version_major()}.{get_version_minor()}.{get_version_patch}" - - def prefix(): return "ABC3D" - -import datetime import time - - +import datetime +from mathutils import ( + Vector, + ) def get_timestamp(): - return datetime.datetime.fromtimestamp(time.time()).strftime("%Y.%m.%d-%H:%M:%S") - + return datetime.datetime \ + .fromtimestamp(time.time()) \ + .strftime('%Y.%m.%d-%H:%M:%S') def mapRange(in_value, in_min, in_max, out_min, out_max, clamp=False): output = out_min + ((out_max - out_min) / (in_max - in_min)) * (in_value - in_min) @@ -39,43 +32,36 @@ def mapRange(in_value, in_min, in_max, out_min, out_max, clamp=False): return output -import functools import warnings - +import functools def deprecated(func): """This is a decorator which can be used to mark functions as deprecated. It will result in a warning being emitted when the function is used.""" - @functools.wraps(func) def new_func(*args, **kwargs): - warnings.simplefilter("always", DeprecationWarning) # turn off filter - warnings.warn( - "Call to deprecated function {}.".format(func.__name__), - category=DeprecationWarning, - stacklevel=2, - ) - warnings.simplefilter("default", DeprecationWarning) # reset filter + warnings.simplefilter('always', DeprecationWarning) # turn off filter + warnings.warn("Call to deprecated function {}.".format(func.__name__), + category=DeprecationWarning, + stacklevel=2) + warnings.simplefilter('default', DeprecationWarning) # reset filter return func(*args, **kwargs) - return new_func import subprocess import sys - - def open_file_browser(directory): - if sys.platform == "win32": + if sys.platform=='win32': os.startfile(directory) - - elif sys.platform == "darwin": - subprocess.Popen(["open", directory]) - + + elif sys.platform=='darwin': + subprocess.Popen(['open', directory]) + else: try: - subprocess.Popen(["xdg-open", directory]) + subprocess.Popen(['xdg-open', directory]) except OSError: pass # er, think of something else to try @@ -87,28 +73,28 @@ def printerr(*args, **kwargs): def removeNonAlphabetic(s): - return "".join([i for i in s if i.isalpha()]) + return ''.join([i for i in s if i.isalpha()]) # # Evaluate a bezier curve for the parameter 0<=t<=1 along its length # def evaluateBezierPoint(p1, h1, h2, p2, t): -# return ((1 - t)**3) * p1 + (3 * t * (1 - t)**2) * h1 + (3 * (t**2) * (1 - t)) * h2 + (t**3) * p2 + # return ((1 - t)**3) * p1 + (3 * t * (1 - t)**2) * h1 + (3 * (t**2) * (1 - t)) * h2 + (t**3) * p2 # # Evaluate the unit tangent on a bezier curve for t # def evaluateBezierTangent(p1, h1, h2, p2, t): -# return ( -# (-3 * (1 - t)**2) * p1 + (-6 * t * (1 - t) + 3 * (1 - t)**2) * h1 + -# (-3 * (t**2) + 6 * t * (1 - t)) * h2 + (3 * t**2) * p2 -# ).normalized() + # return ( + # (-3 * (1 - t)**2) * p1 + (-6 * t * (1 - t) + 3 * (1 - t)**2) * h1 + + # (-3 * (t**2) + 6 * t * (1 - t)) * h2 + (3 * t**2) * p2 + # ).normalized() # def calculateBezierLength(p1, h1, h2, p2, resolution=20): -# step = 1/resolution -# previous_p = p1 -# length = 0 -# for i in range(0, resolution): -# t = (i + 1) * step -# p = evaluateBezierPoint(p1, h1, h2, p2, t) -# length += p.distance(previous_p) -# previous_p = p -# return length + # step = 1/resolution + # previous_p = p1 + # length = 0 + # for i in range(0, resolution): + # t = (i + 1) * step + # p = evaluateBezierPoint(p1, h1, h2, p2, t) + # length += p.distance(previous_p) + # previous_p = p + # return length