# SPDX-License-Identifier: GPL-2.0-only """ 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 bl_info = { "name": "ABC3D", "author": "Jakob Schlötter, Studio Pointer*", "version": (0, 0, 11), "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 # make sure that modules are reloadable # when registering # handy for development # first import dependencies for the method if "Font" in locals(): importlib.reload(Font) importlib.reload(utils) importlib.reload(butils) importlib.reload(bimport) importlib.reload(addon_updater_ops) def getPreferences(context): preferences = context.preferences return preferences.addons[__name__].preferences @addon_updater_ops.make_annotations class ABC3D_addonPreferences(bpy.types.AddonPreferences): """ABC3D Addon Preferences These are the preferences at Edit/Preferences/Add-ons""" bl_idname = __name__ # Addon updater preferences. auto_check_update = bpy.props.BoolProperty( name="Auto-check for Update", description="If enabled, auto-check for updates using an interval", default=False, ) updater_interval_months = bpy.props.IntProperty( name="Months", description="Number of months between checking for updates", default=0, min=0, ) updater_interval_days = bpy.props.IntProperty( name="Days", description="Number of days between checking for updates", default=7, min=0, max=31, ) updater_interval_hours = bpy.props.IntProperty( name="Hours", description="Number of hours between checking for updates", default=0, min=0, max=23, ) updater_interval_minutes = bpy.props.IntProperty( name="Minutes", description="Number of minutes between checking for updates", default=0, min=0, max=59, ) def get_default_assets_dir(): 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.", ), ) 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.", ), ) print(f"{__name__}: change assets_dir to {self.assets_dir}") assets_dir: bpy.props.StringProperty( name="Assets Folder", subtype="DIR_PATH", default=get_default_assets_dir(), update=on_change_assets_dir, ) def draw(self, context): layout = self.layout layout.label(text="Directory for storage of fonts and other assets:") layout.prop(self, "assets_dir") # Updater draw function, could also pass in col as third arg. addon_updater_ops.update_settings_ui(self, context) class ABC3D_available_font(bpy.types.PropertyGroup): font_name: bpy.props.StringProperty(name="") face_name: bpy.props.StringProperty(name="") 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] # ) t = butils.get_text_properties(self.text_id) if t is not None: butils.set_text_on_curve(t) 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(): items.append((f"{f[0]} {f[1]}", f"{f[0]} {f[1]}", "")) return items 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) def update_callback(self, context): try: butils.set_text_on_curve(self) except (AttributeError, TypeError): butils.set_text_on_curve(self, can_regenerate=True) def font_update_callback(self, context): font_name, face_name = self.font.split(" ") self["font_name"] = font_name self["face_name"] = face_name self.glyphs_update_callback(self) text_id: bpy.props.IntProperty() font: bpy.props.EnumProperty( 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) text_object: bpy.props.PointerProperty(type=bpy.types.Object) text: bpy.props.StringProperty(update=glyphs_update_callback) letter_spacing: bpy.props.FloatProperty( update=update_callback, name="Letter Spacing", description="Letter Spacing", 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", ) translation: bpy.props.FloatVectorProperty( update=update_callback, name="Translation", default=(0.0, 0.0, 0.0), subtype="TRANSLATION", ) font_size: bpy.props.FloatProperty( update=update_callback, name="Font Size", default=1.0, subtype="NONE", ) offset: bpy.props.FloatProperty( update=update_callback, name="Offset", default=0.0, subtype="NONE", ) compensate_curvature: bpy.props.BoolProperty( update=update_callback, name="Compensate Curvature", description="Fixes curvature spacing issues for simple curves, don't use on curve with tiny loops.", default=True, ) ignore_orientation: bpy.props.BoolProperty( update=update_callback, 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) actual_text: bpy.props.StringProperty() class ABC3D_data(bpy.types.PropertyGroup): available_fonts: bpy.props.CollectionProperty( type=ABC3D_available_font, name="Available fonts" ) def active_font_index_update(self, context): if len(self.available_fonts) <= self.active_font_index: self.active_font_index = len(self.available_fonts) - 1 active_font_index: bpy.props.IntProperty( default=-1, update=active_font_index_update, ) available_texts: bpy.props.CollectionProperty( type=ABC3D_text_properties, name="Available texts" ) def active_text_index_update(self, context): lock_depsgraph_updates() if self.active_text_index != -1: text_properties = butils.get_text_properties_by_index( self.active_text_index, context.scene ) if text_properties is not None: o = text_properties.text_object # active_text_index changed. so let's update the selection # check if it is already selected # or perhaps one of the glyphs if os is not None and not butils.is_or_has_parent( context.active_object, o ): # if ( # o is not None # and 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) context.view_layer.objects.active = o unlock_depsgraph_updates() # else: # print("already selected") active_text_index: bpy.props.IntProperty(update=active_text_index_update) # def font_path_update_callback(self, context): # butils.ShowMessageBox("Friendly Reminder", message="do not forget to click on 'Install new font'") # bpy.ops.abc3d.install_font() # stupid hack for Mac OS font_path: bpy.props.StringProperty( name="Font path", description="Install a *.glb or *.gltf fontfile from disk", default="", maxlen=1024, # update=font_path_update_callback, 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", ) class ABC3D_UL_fonts(bpy.types.UIList): 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}") def invoke(self, context, event): pass class ABC3D_UL_texts(bpy.types.UIList): 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 split.label(text=f"{item.text}") def invoke(self, context, event): pass class ABC3D_PT_Panel(bpy.types.Panel): bl_label = f"{utils.prefix()} Panel" bl_category = "ABC3D" bl_space_type = "VIEW_3D" bl_region_type = "UI" def draw(self, context): layout = self.layout row = layout.row() row.label(text=f"{utils.prefix()} v{utils.get_version_string()}") icon = "NONE" if len(context.scene.abc3d_data.available_fonts) == 0: 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", ) class ABC3D_PT_FontList(bpy.types.Panel): bl_label = "Font List" bl_parent_id = "ABC3D_PT_Panel" bl_category = "ABC3D" bl_space_type = "VIEW_3D" bl_region_type = "UI" def draw(self, context): layout = self.layout wm = context.window_manager scene = context.scene 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", ) if ( abc3d_data.active_font_index >= 0 and len(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 face: Font.FontFace = Font.get_font_face(font_name, face_name) if face is not None: available_glyphs = face.glyphs_in_fontfile loaded_glyphs = sorted(face.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:") 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 ] ) scale_y = 0.5 row = subbox.row() row.scale_y = scale_y row.alignment = "CENTER" row.label(text=text) n_rows = int(len(loaded_glyphs) / n) box.row().label(text="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 ] ) 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.font_name = font_name oper_lf.face_name = face_name box = layout.box() row = box.row() row.label(text="File and Memory optimization") row = box.row() row.operator(f"{__name__}.refresh_fonts", text="Refresh font list from disk") row = box.row() row.operator(f"{__name__}.unload_unused_glyphs", text="Unload unused glyphs") class ABC3D_PT_TextPlacement(bpy.types.Panel): bl_label = "Place Text" bl_parent_id = "ABC3D_PT_Panel" bl_category = "ABC3D" bl_space_type = "VIEW_3D" bl_region_type = "UI" can_place = False @classmethod def poll(self, context): if context.active_object is not None and context.active_object.type == "CURVE": self.can_place = True else: self.can_place = False return True def draw(self, context): layout = self.layout wm = context.window_manager scene = context.scene abc3d_data = scene.abc3d_data placerow = layout.row() placerow.enabled = self.can_place 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.") class ABC3D_PT_TextManagement(bpy.types.Panel): bl_label = "Text Management" bl_parent_id = "ABC3D_PT_Panel" bl_category = "ABC3D" bl_space_type = "VIEW_3D" bl_region_type = "UI" bl_options = {"DEFAULT_CLOSED"} def draw(self, context): layout = self.layout wm = context.window_manager scene = context.scene 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") 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" bl_category = "ABC3D" bl_space_type = "VIEW_3D" bl_region_type = "UI" bl_options = {"DEFAULT_CLOSED"} def draw(self, context): layout = self.layout wm = context.window_manager 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" ) 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") box.row().operator( 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") class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): bl_label = "Text Properties" bl_parent_id = "ABC3D_PT_TextManagement" bl_category = "ABC3D" bl_space_type = "VIEW_3D" bl_region_type = "UI" 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()}_text_id" in a_o: text_index = a_o[f"{utils.prefix()}_text_id"] return bpy.context.scene.abc3d_data.available_texts[text_index] elif a_o.parent is not None and f"{utils.prefix()}_text_id" in a_o.parent: text_index = a_o.parent[f"{utils.prefix()}_text_id"] 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 return None def get_active_glyph_properties(self): a_o = bpy.context.active_object if a_o is not None: if ( f"{utils.prefix()}_text_id" in a_o and f"{utils.prefix()}_glyph_index" in a_o ): text_index = a_o[f"{utils.prefix()}_text_id"] 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 @classmethod def poll(self, context): try: return self.get_active_text_properties(self) is not None except IndexError: return False def draw(self, context): layout = self.layout props = self.get_active_text_properties() glyph_props = self.get_active_glyph_properties() if props is None or props.text_object is 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}") layout.row().prop(props, "font") layout.row().prop(props, "text") layout.row().prop(props, "letter_spacing") layout.row().prop(props, "font_size") 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_RefreshAvailableFonts(bpy.types.Operator): """Refreshes available font list from disk. This also removes all fonts which are not saved in the asset directory. Can be useful when creating fonts or manually installing fonts.""" bl_idname = f"{__name__}.refresh_fonts" bl_label = "Refresh Available Fonts" bl_options = {"REGISTER", "UNDO"} def execute(self, context): refresh_fonts() return {"FINISHED"} class ABC3D_OT_UnloadUnusedGlyphs(bpy.types.Operator): """Unload all glyphs which are not actively used in this project from memory. They will still be normally loaded when you use them again.""" bl_idname = f"{__name__}.unload_unused_glyphs" bl_label = "Unload Unused Glyphs" bl_options = {"REGISTER", "UNDO"} def execute(self, context): butils.unload_unused_glyphs() return {"FINISHED"} class ABC3D_OT_InstallFont(bpy.types.Operator): """Install or load Fontfile from path above. (Format must be *.glb or *.gltf)""" bl_idname = f"{__name__}.install_font" bl_label = "Load Font" 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") else: print(f"font_path_update: {font_path} does not exist") font_path: bpy.props.StringProperty( name="Font path", description="Install a *.glb or *.gltf fontfile from disk", default="", maxlen=1024, update=font_path_update_callback, subtype="FILE_PATH", ) install_in_assets: bpy.props.BoolProperty( name="install in assets", description="install the font in the assets directory of the addon", 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, ) def draw(self, context): abc3d_data = context.scene.abc3d_data layout = self.layout layout.row().prop(self, "font_path") # layout.row().prop(abc3d_data, "font_path") # closes the stupid panel on Mac OS.. 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="the fontfile should not be moved.") layout.row().prop(self, "load_into_memory") if self.load_into_memory: layout.label(text="Loading font files can take a long time") layout.label(text="and use a lot of RAM.") layout.label(text="We recommend not doing this and let us") layout.label(text="load the font data on demand.") 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) return context.window_manager.invoke_props_dialog(self) def execute(self, context): scene = bpy.context.scene abc3d_data = context.scene.abc3d_data font_path = butils.bpy_to_abspath(self.font_path) if not os.path.exists(font_path): butils.ShowMessageBox( title=f"{__name__} Warning", icon="ERROR", message=[ "Could not install font.", f"We believe the font path ({font_path}) does not exist.", "If this is an error, please let us know.", ], ) 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): # print(f"registering installed fonts") # bpy.app.timers.register(lambda: register_load(target, self.load_into_memory), first_interval=5) butils.register_font_from_filepath(target) if self.load_into_memory: butils.load_font_from_filepath(target) butils.update_available_fonts() else: butils.register_font_from_filepath(font_path) if self.load_into_memory: butils.load_font_from_filepath(font_path) 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"} 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"} 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"} 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"} 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 layout.row().prop(self, "load_into_memory") if self.load_into_memory: layout.label(text="Loading font files can take a long time") layout.label(text="and use a lot of RAM.") layout.label(text="We recommend not doing this and let us") layout.label(text="load the font data on demand.") def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self) def execute(self, context): scene = bpy.context.scene if self.load_into_memory: butils.load_installed_fonts() else: butils.register_installed_fonts() butils.ShowMessageBox("Loading Fonts", "INFO", "Updating Data Structures.") butils.update_available_fonts() butils.ShowMessageBox("Loading Fonts", "INFO", "Done loading installed fonts.") 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"} font_name: bpy.props.StringProperty() face_name: bpy.props.StringProperty() def execute(self, context): face: Font.FontFace = Font.get_font_face(self.font_name, self.face_name) if face is None: butils.ShowMessageBox( f"{utils.prefix()} Load Font", icon="ERROR", message=[ "Could not load font, sorry!", f"{self.font_name=} {self.face_name=}", ], ) return {"CANCELLED"} filepaths = face.filepaths for f in filepaths: butils.load_font_from_filepath(f) 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"} def execute(self, context): objects = bpy.context.selected_objects butils.add_default_metrics_to_objects(objects) 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"} def execute(self, context): objects = bpy.context.selected_objects butils.remove_metrics_from_objects(objects) 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. """ bl_idname = f"{__name__}.align_metrics_to_active_object" bl_label = "Align metrics to active object" bl_options = {"REGISTER", "UNDO"} def execute(self, context): objects = bpy.context.selected_objects butils.align_metrics_of_objects_to_active_object(objects) 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.""" bl_idname = f"{__name__}.align_metrics" bl_label = "Align metrics" bl_options = {"REGISTER", "UNDO"} def execute(self, context): objects = bpy.context.selected_objects butils.align_metrics_of_objects(objects) 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"} def execute(self, context): global shared scene = bpy.context.scene abc3d_data = scene.abc3d_data # butils.load_font_from_filepath("/home/jrkb/.config/blender/4.1/datafiles/abc3d/fonts/NM_Origin.glb") butils.update_available_fonts() # objects = bpy.context.selected_objects # butils.add_default_metrics_to_objects(objects) # reference_bound_box = None # for o in objects: # bb = o.bound_box # reference_bound_box = butils.get_max_bound_box(bb, reference_bound_box) # for o in objects: # metrics = butils.get_metrics_bound_box(o.bound_box, reference_bound_box) # 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"} class ABC3D_OT_RemoveText(bpy.types.Operator): """Remove Text 3D""" bl_idname = f"{__name__}.remove_text" bl_label = "Remove Text" bl_options = {"REGISTER", "UNDO"} remove_objects: bpy.props.BoolProperty( name="Remove Objects", description="Remove both ABC3D text functionality and the objects/meshes", default=True, ) remove_custom_properties: bpy.props.BoolProperty( name="Remove Custom Properties", description="Remove ABC3D custom properties of objects", default=True, ) def invoke(self, context, event): wm = context.window_manager return wm.invoke_props_dialog(self) def execute(self, context): abc3d_data = context.scene.abc3d_data lock_depsgraph_updates() if abc3d_data.active_text_index < 0: butils.ShowMessageBox( title="No text selected", message=("Please select a text."), icon="GHOST_ENABLED", ) return {"CANCELLED"} i = abc3d_data.active_text_index if abc3d_data.available_texts[i].text_object is not None: mom = abc3d_data.available_texts[i].text_object if self.remove_custom_properties: def delif(o, p): if p in o: del o[p] delif(mom, f"{utils.prefix()}_type") delif(mom, f"{utils.prefix()}_text_id") delif(mom, f"{utils.prefix()}_font_name") delif(mom, f"{utils.prefix()}_face_name") delif(mom, f"{utils.prefix()}_font_size") delif(mom, f"{utils.prefix()}_letter_spacing") delif(mom, f"{utils.prefix()}_orientation") delif(mom, f"{utils.prefix()}_translation") delif(mom, f"{utils.prefix()}_offset") if self.remove_objects: remove_list = [] for g in abc3d_data.available_texts[i].glyphs: if g is not None: remove_list.append(g.glyph_object) butils.simply_delete_objects(remove_list) abc3d_data.available_texts.remove(i) unlock_depsgraph_updates() 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"} 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 def font_update_callback(self, context): font_name, face_name = self.font.split(" ") 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) text: bpy.props.StringProperty( name="Text", description="The text.", default="ABC3D", maxlen=1024, ) # target_object: bpy.props.PointerProperty( # name="The Target Object", # description="The target, which will be populated by character children of text.", # type=bpy.types.Object, # ) letter_spacing: bpy.props.FloatProperty( name="Letter Spacing", description="Letter Spacing", default=0.0, ) font_size: bpy.props.FloatProperty( name="Font Size", default=1.0, subtype="NONE", ) offset: bpy.props.FloatProperty( name="Offset", default=0.0, subtype="NONE", ) translation: bpy.props.FloatVectorProperty( name="Translation", default=(0.0, 0.0, 0.0), subtype="TRANSLATION", ) orientation: bpy.props.FloatVectorProperty( name="Orientation", default=(1.5707963267948966, 0.0, 0.0), # 90 degrees in radians subtype="EULER", ) def invoke(self, context, event): wm = context.window_manager self.font_update_callback(context) return wm.invoke_props_dialog(self) def execute(self, context): global shared scene = bpy.context.scene abc3d_data = scene.abc3d_data selected = bpy.context.view_layer.objects.active if selected: # font = abc3d_data.available_fonts[abc3d_data.active_font_index] # font_name = font.font_name # face_name = font.face_name distribution_type = "DEFAULT" text_id = butils.find_free_text_id() 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['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 = self.font # enums want to be set as attribute # this also calls the update function # so we don't need to prepare/set again # no need for these: # butils.prepare_text(t.font_name, # t.face_name, # t.text) # or this: # butils.set_text_on_curve(t) else: butils.ShowMessageBox( title="No object selected", message=( "Please select an object.", "It will be used to put the type on.", "Thank you :)", ), icon="GHOST_ENABLED", ) 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. """ bl_idname = f"{__name__}.toggle_abc3d_collection" bl_label = "Toggle Collection visibility" bl_options = {"REGISTER", "UNDO"} def execute(self, context): scene = context.scene fontcollection = bpy.data.collections.get("ABC3D") if fontcollection is None: self.report( {"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") else: scene.collection.children.unlink(fontcollection) self.report({"INFO"}, f"{bl_info['name']}: hide collection") 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"} can_execute: bpy.props.BoolProperty(default=True) create_output_directory: bpy.props.BoolProperty(default=False) 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") 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", ) available_font = abc3d_data.available_fonts[abc3d_data.active_font_index] font_name = available_font.font_name face_name = available_font.face_name face: Font.FontFace = Font.get_font_face(font_name, face_name) if face is not None: loaded_glyphs = sorted(face.loaded_glyphs) n = 16 n_rows = int(len(loaded_glyphs) / n) box = layout.box() box.row().label(text="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 ] ) scale_y = 0.5 row = subbox.row() row.scale_y = scale_y row.label(text=text) row = layout.row() export_dir = butils.bpy_to_abspath(abc3d_data.export_dir) if os.access(export_dir, os.W_OK): self.can_execute = True elif os.path.exists(export_dir): self.can_execute = False row.alert = True row.label(text="Export directory exists but is not writable") row = layout.row() row.alert = True row.label(text="Please select another directory") row = layout.row() row.alert = True elif not utils.can_create_path( export_dir ): # does not exist and cannot be created self.can_execute = False row.alert = True row.label(text="Directory does not exist and cannot be created") row = layout.row() row.alert = True row.label(text="Please select another directory") row = layout.row() row.alert = True elif utils.can_create_path(export_dir): # does not exist and can be created self.can_execute = True row.label(text="Directory does not exist") row = layout.row() row.label(text="But can and will be created on export") row = layout.row() else: self.can_execute = False row.alert = True row.label(text="Please select another directory") row = layout.row() row.alert = True row.prop(abc3d_data, "export_dir") else: print( f"{utils.prefix()}::save_font_to_file ERROR {face=} {font_name=} {face_name=}" ) print(f"{utils.prefix()} {Font.fonts=}") def execute(self, context): global shared scene = bpy.context.scene abc3d_data = scene.abc3d_data if not self.can_execute: butils.ShowMessageBox( "Cannot export font", "ERROR", [ f"export directory '{abc3d_data.export_dir}' does not exist or is not writable", "try setting another path", ], ) return {"CANCELLED"} if not os.path.exists(butils.bpy_to_abspath(abc3d_data.export_dir)): path = butils.bpy_to_abspath(abc3d_data.export_dir) if utils.can_create_path(path): os.makedirs(path, exist_ok=True) else: butils.ShowMessageBox( "Cannot export font", "ERROR", [ f"export directory '{abc3d_data.export_dir}' does not exist and cannot be created", "try setting another path", ], ) return {"CANCELLED"} fontcollection = bpy.data.collections.get("ABC3D") # check if all is good to proceed if fontcollection is None: 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"} 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"} # save state to restore later was_fontcollection_linked = ( scene.collection.children.find(fontcollection.name) >= 0 ) was_selection = [] for obj in bpy.context.selected_objects: was_selection.append(obj) was_active_object = bpy.context.view_layer.objects.active bpy.ops.object.select_all(action="DESELECT") # get save data selected_font = abc3d_data.available_fonts[abc3d_data.active_font_index] # print(selected_font.font_name) self.report( {"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") linked_collections = bpy.context.scene.collection.children.values() for c in linked_collections: bpy.context.scene.collection.children.unlink(c) bpy.context.scene.collection.children.link(fontcollection) # select what needs to be selected export_objects = [] for obj in fontcollection.objects: if obj["font_name"] == selected_font.font_name: if not butils.is_metrics_object(obj): obj.select_set(True) export_objects.append(obj) else: obj.select_set(True) butils.add_faces_to_metrics(obj) export_objects.append(obj) context_override = bpy.context.copy() context_override["selected_objects"] = list(export_objects) # context_override["scene"] = bpy.context.scene.copy() with bpy.context.temp_override(**context_override): filepath = f"{abc3d_data.export_dir}/{selected_font.font_name}.glb" # get rid of scene extra data before export scene_keys = [] for k in bpy.context.scene.keys(): scene_keys.append(k) for k in scene_keys: del bpy.context.scene[k] # save as gltf bpy.ops.export_scene.gltf( filepath=filepath, check_existing=False, # GLB or GLTF_SEPARATE (also change filepath) export_format="GLB", export_extras=True, use_selection=True, use_active_scene=True, ) def delete_scene(): bpy.ops.scene.delete() return None bpy.app.timers.register(lambda: delete_scene(), first_interval=1) # bpy.ops.scene.delete() # restore() def remove_faces(): for obj in fontcollection.objects: 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"}, f"{utils.prefix()}::save_font_to_file done") 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) 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"} font_name: bpy.props.StringProperty( default="NM_Origin", ) face_name: bpy.props.StringProperty( default="Tender", ) autodetect_names: bpy.props.BoolProperty( default=True, ) fix_common_misspellings: bpy.props.BoolProperty( default=True, ) def invoke(self, context, event): 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: layout.row().label(text="No objects selected.", icon="ERROR") 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 ) 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 = layout.row() row.scale_y = scale_y row.label(text="'__'") row = layout.row() row.scale_y = scale_y row.label(text=" - glyph id: unicode glyph name or raw glyph") row = layout.row() row.scale_y = scale_y row.label(text=" - font name: font name with underscore") row = layout.row() row.scale_y = scale_y row.label(text=" - face name: face name") row = layout.row() row.scale_y = scale_y row.label(text="working examples:") row = layout.row() row.scale_y = scale_y row.label(text="- 'A_NM_Origin_Tender'") row = layout.row() row.scale_y = scale_y row.label(text="- 'B_NM_Origin_Tender'") row = layout.row() row.scale_y = scale_y row.label(text="- 'arrowright_NM_Origin_Tender'") row = layout.row() row.scale_y = scale_y row.label(text="- '→_NM_Origin_Tender' (equal to above)") row = layout.row() row.scale_y = scale_y row.label(text="- 'quotesingle_NM_Origin_Tender.001'") row = layout.row() row.scale_y = scale_y 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") 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]]})" ) row = layout.row() row.scale_y = 0.5 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"} global shared scene = bpy.context.scene abc3d_data = scene.abc3d_data fontcollection = bpy.data.collections.get("ABC3D") if fontcollection is None: fontcollection = bpy.data.collections.new("ABC3D") font_name = self.font_name face_name = self.face_name currentObjects = [] for o in context.selected_objects: if o.name not in currentObjects: print(f"processing {o.name}") process_object = True if self.autodetect_names: font_name, face_name = self.do_autodetect_names(o.name) if butils.is_mesh(o) and not butils.is_metrics_object(o): uc = o.users_collection if self.fix_common_misspellings: 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] glyph_id = Font.name_to_glyph(name) o.name = f"{name}_{font_name}_{face_name}" if glyph_id is not 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) Font.add_glyph( 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: found = True break if not found: f = abc3d_data.available_fonts.add() f.font_name = font_name f.face_name = face_name else: print(f"import warning: did not understand glyph {name}") self.report({"INFO"}, f"did not understand glyph {name}") return {"FINISHED"} class ABC3D_OT_Reporter(bpy.types.Operator): bl_idname = f"{__name__}.reporter" bl_label = "Report" label = bpy.props.StringProperty( name="label", default="INFO", ) message = bpy.props.StringProperty( name="message", default="I have nothing to say really", ) def execute(self, context): # this is where I send the message self.report({"INFO"}, "whatever") for i in range(0, 10): butils.ShowMessageBox("whatever", "INFO", "INFO") return {"FINISHED"} classes = ( bimport.ImportGLTF2, bimport.GetFontFacesInFile, ABC3D_addonPreferences, ABC3D_available_font, ABC3D_glyph_properties, ABC3D_text_properties, ABC3D_data, ABC3D_UL_fonts, ABC3D_UL_texts, ABC3D_PT_Panel, ABC3D_PT_FontList, 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_RefreshAvailableFonts, ABC3D_OT_UnloadUnusedGlyphs, ABC3D_OT_LoadInstalledFonts, ABC3D_OT_LoadFont, ABC3D_OT_AddDefaultMetrics, 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, ABC3D_OT_InstallFont, ABC3D_OT_ToggleABC3DCollection, ABC3D_OT_SaveFontToFile, ABC3D_OT_CreateFontFromObjects, ABC3D_OT_Reporter, ) def compare_text_object_with_object(t, o, strict=False): for k in o.keys(): if k == f"{utils.prefix()}_type": if o[k] != "textobject": return False elif k.startswith(f"{utils.prefix()}_"): p = k.replace(f"{utils.prefix()}_", "") if p in t.keys(): if t[p] != o[k]: return False else: print(f"{__name__} set_text_object: did not find key ({p})") if strict: return False # for p in t.keys(): # if return True def link_text_object_with_new_text_properties(text_object, scene=None): lock_depsgraph_updates() butils.link_text_object_with_new_text_properties(text_object, scene) unlock_depsgraph_updates() def determine_active_text_index_from_selection(): if bpy.context.active_object is None: return -1 for text_index, text_properties in enumerate( bpy.context.scene.abc3d_data.available_texts ): if butils.is_text_object_legit(text_properties.text_object): if butils.is_or_has_parent( bpy.context.active_object, text_properties.text_object ): return text_index return -1 def update_active_text_index(): text_index = determine_active_text_index_from_selection() if text_index != bpy.context.scene.abc3d_data.active_text_index: bpy.context.scene.abc3d_data.active_text_index = text_index def detect_text(): lock_depsgraph_updates() scene = bpy.context.scene abc3d_data = scene.abc3d_data required_keys = [ "type", "text_id", "font_name", "face_name", "text", ] objects = scene.objects for o in objects: valid = True for key in required_keys: if butils.get_key(key) not in o: valid = False break if not valid: continue if o[butils.get_key("type")] == "textobject": current_text_id = int(o[butils.get_key("text_id")]) text_properties = butils.get_text_properties(current_text_id) if text_properties is not None and text_properties.text_object == o: # all good pass else: butils.link_text_object_with_new_text_properties(o, scene) unlock_depsgraph_updates() 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) if isinstance(a, int): if a == Font.MISSING_FONT: 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?", ], ) elif len(a.unloaded) > 0: for fp in a.filepaths: butils.load_font_from_filepath(fp, a.unloaded) def refresh_fonts(): fontcollection: bpy_types.Collection = bpy.data.collections.get("ABC3D") if fontcollection is not None: objs = [o for o in fontcollection.objects if o.parent == None] butils.completely_delete_objects(objs) butils.run_in_main_thread(Font.fonts.clear) butils.run_in_main_thread(butils.clear_available_fonts) butils.run_in_main_thread(butils.register_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) butils.run_in_main_thread(detect_text) butils.run_in_main_thread(butils.unload_unused_glyphs) @persistent 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(load_used_glyphs) butils.run_in_main_thread(butils.update_types) def load_handler_unload(): if bpy.app.timers.is_registered(butils.execute_queued_functions): bpy.app.timers.unregister(butils.execute_queued_functions) @persistent def on_frame_changed(self, dummy): for t in bpy.context.scene.abc3d_data.available_texts: # TODO PERFORMANCE: only on demand butils.set_text_on_curve(t) depsgraph_updates_locked = 0 def unlock_depsgraph_updates(): global depsgraph_updates_locked depsgraph_updates_locked -= 1 def lock_depsgraph_updates(auto_unlock_s=-1): global depsgraph_updates_locked depsgraph_updates_locked += 1 if auto_unlock_s >= 0: if bpy.app.timers.is_registered(unlock_depsgraph_updates): bpy.app.timers.unregister(unlock_depsgraph_updates) bpy.app.timers.register(unlock_depsgraph_updates, first_interval=auto_unlock_s) def are_depsgraph_updates_locked(): global depsgraph_updates_locked return depsgraph_updates_locked > 0 import time @persistent def on_depsgraph_update(scene, depsgraph): if not bpy.context.mode.startswith("EDIT") and not are_depsgraph_updates_locked(): lock_depsgraph_updates(auto_unlock_s=-1) for u in depsgraph.updates: if ( butils.get_key("text_id") in u.id.keys() and butils.get_key("type") in u.id.keys() and u.id[butils.get_key("type")] == "textobject" ): text_id = u.id[butils.get_key("text_id")] # if u.is_updated_geometry: text_properties = butils.get_text_properties(text_id) if text_properties is not None: if text_properties.text_object == u.id.original: # nothing to do pass elif butils.is_text_object_legit(u.id.original): # must be duplicate link_text_object_with_new_text_properties(u.id.original, scene) elif ( butils.is_text_object_legit(u.id.original) and len(u.id.original.users_collection) > 0 ): # must be a new thing, maybe manually created or so link_text_object_with_new_text_properties(u.id.original, scene) butils.clean_text_properties() update_active_text_index() unlock_depsgraph_updates() def register(): print(f"REGISTER {utils.prefix()}") addon_updater_ops.register(bl_info) for cls in classes: 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 if load_handler not in bpy.app.handlers.load_post: bpy.app.handlers.load_post.append(load_handler) # 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_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(Font.fonts.clear) butils.run_in_main_thread(butils.clear_available_fonts) butils.run_in_main_thread(butils.register_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) butils.run_in_main_thread(detect_text) Font.init() def unregister(): addon_updater_ops.unregister() for cls in reversed(classes): bpy.utils.unregister_class(cls) # remove autostart when loading blend file if load_handler in bpy.app.handlers.load_post: bpy.app.handlers.load_post.remove(load_handler) # 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_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__": register()