# SPDX-License-Identifier: GPL-2.0-only """ A 3D font helper """ import os from bpy.app.handlers import persistent 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, 2), "blender": (4, 1, 0), "location": "VIEW3D", "description": "Convenience addon for 3D fonts", "category": "Typography", } # 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) else: from .common import Font from .common import utils from . import butils from . import bimport from . import 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") # 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) 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): glyph_id: bpy.props.StringProperty(maxlen=1) glyph_object: bpy.props.PointerProperty(type=bpy.types.Object) letter_spacing: bpy.props.FloatProperty( name="Letter Spacing", description="Letter Spacing", ) 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 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] return 0 # f"{f.font_name} {f.face_name}" else: f = d.available_fonts[0] return 0 # f"{f.font_name} {f.face_name}" 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) def update_callback(self, context): butils.set_text_on_curve(self) 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, ) distribution_type: bpy.props.StringProperty() glyphs: bpy.props.CollectionProperty(type=ABC3D_glyph_properties) # TODO: simply, merge, cut cut cut 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): if self.active_text_index != -1: o = self.available_texts[self.active_text_index].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 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") 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=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): # 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"{__name__} panel" bl_category = "ABC3D" bl_space_type = "VIEW_3D" bl_region_type = "UI" def draw(self, context): layout = self.layout 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_LoadFontPanel(bpy.types.Panel): # bl_label = "Install a new font" # 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 # box = layout.box() # box.row().label(text="1. Select fontfile") # box.row().prop(context.scene.abc3d_data, "font_path") # box.row().label(text="2. Install it:") # box.row().operator(f"{__name__}.install_font", text='Install new font') 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: 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) 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=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]) 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=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]) scale_y = 0.5 row = subbox.row() row.scale_y = scale_y row.label(text=text) row = layout.row() oper = row.operator(f"{__name__}.load_font", text='Load all glyphs in memory') oper.font_name = font_name oper.face_name = face_name 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 type(context.active_object) != type(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"} # TODO: perhaps this should be done in a periodic timer @classmethod def poll(self, context): scene = context.scene abc3d_data = scene.abc3d_data # TODO: update available_texts def update(): if bpy.context.screen.is_animation_playing: return active_text_index = -1 remove_list = [] for i, t in enumerate(abc3d_data.available_texts): if type(t.text_object) == type(None): remove_list.append(i) continue remove_me = True for c in t.text_object.children: 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) # 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 remove_me: remove_list.append(i) for i in remove_list: if type(abc3d_data.available_texts[i].text_object) != type(None): mom = abc3d_data.available_texts[i].text_object 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") 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") abc3d_data.available_texts.remove(i) 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): active_text_index = i if active_text_index != abc3d_data.active_text_index: abc3d_data.active_text_index = active_text_index # butils.run_in_main_thread(update) return True 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_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 scene = context.scene abc3d_data = scene.abc3d_data layout.row().operator( 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') 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') 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(): 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 # 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_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 # 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: bpy.props.EnumProperty( # items=font_items_callback, # default=font_default_callback, # update=font_update_callback, # ) @classmethod def poll(self, context): return type(self.get_active_text_properties(self)) != type(None) def draw(self, context): layout = self.layout wm = context.window_manager scene = context.scene abc3d_data = scene.abc3d_data props = self.get_active_text_properties() 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 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.column().prop(props, "translation") layout.column().prop(props, "orientation") 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): if os.path.exists(self.font_path): print(f"{self.font_path} does exist") else: print(f"{self.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=[ f"Could not install font.", f"We believe the font path ({font_path}) does not exist.", f"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): print("EXECUTE LOAD INSTALLED FONTS") 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): filepaths = Font.fonts[self.font_name].faces[self.face_name].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""" 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""" 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_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) 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 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 type(abc3d_data.available_texts[i].text_object) != type(None): mom = abc3d_data.available_texts[i].text_object 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") 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 type(g) != type(None): remove_list.append(g.glyph_object) butils.simply_delete_objects(remove_list) abc3d_data.available_texts.remove(i) 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 = 0 for i, tt in enumerate(abc3d_data.available_texts): while text_id == tt.text_id: text_id = text_id + 1 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""" 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'} 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 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=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]) scale_y = 0.5 row = subbox.row() row.scale_y = scale_y row.label(text=text) layout.prop(abc3d_data, 'export_dir') def execute(self, context): global shared scene = bpy.context.scene abc3d_data = scene.abc3d_data 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, ) bpy.app.timers.register( lambda: bpy.ops.scene.delete(), 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) bpy.app.timers.register(lambda: remove_faces(), first_interval=2) 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) 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", ) import_infix: bpy.props.StringProperty( default="_NM_Origin_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 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') if self.autodetect_names: scale_y = 0.5 row = layout.row() row.scale_y = scale_y 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') 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]]})" 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") 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 # TODO: do not clear # abc3d_data.available_fonts.clear() # Font.fonts = {} 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: 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 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) 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) Font.add_glyph( font_name, face_name, glyph_id, 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_PT_RightPropertiesPanel(bpy.types.Panel): """Creates a Panel in the Object properties window""" bl_label = f"{bl_info['name']}" bl_idname = "ABC3D_PT_RightPropertiesPanel" bl_space_type = 'PROPERTIES' bl_region_type = 'WINDOW' bl_context = "object" @classmethod def poll(self, context): # only show the panel, if it's a textobject or a glyph is_text = type(next((t for t in context.scene.abc3d_data.available_texts if t.text_object == context.active_object), None)) != type(None) is_glyph = type(next((t for t in context.scene.abc3d_data.available_texts if t.text_object == context.active_object.parent), None)) != type(None) return is_text or is_glyph def draw(self, context): layout = self.layout scene = context.scene abc3d_data = scene.abc3d_data obj = context.active_object def is_it_text(): return type(next((t for t in context.scene.abc3d_data.available_texts if t.text_object == context.active_object), None)) != type(None) def is_it_glyph(): return type(next((t for t in context.scene.abc3d_data.available_texts if t.text_object == context.active_object.parent), None)) != type(None) is_text = is_it_text() is_glyph = is_it_glyph() textobject = obj if is_text else obj.parent if is_glyph else obj available_text = abc3d_data.available_texts[abc3d_data.active_text_index] # row = layout.row() # row.label(text="Hello world!", icon='WORLD_DATA') # row = layout.row() # row.label(text="Active object is: " + obj.name) # row = layout.row() # row.label(text="text object is: " + textobject.name) row = layout.row() row.label(text=f"active text index is: {abc3d_data.active_text_index}") layout.row().label(text="Text Properties:") layout.row().prop(available_text, "text") layout.row().prop(available_text, "letter_spacing") layout.row().prop(available_text, "font_size") layout.row().prop(available_text, "offset") layout.row().prop(available_text, "compensate_curvature") layout.row().prop(available_text, "ignore_orientation") layout.column().prop(available_text, "translation") layout.column().prop(available_text, "orientation") if is_glyph: layout.row().label(text="Glyph Properties:") 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_LoadFontPanel, ABC3D_PT_FontList, ABC3D_PT_TextPlacement, ABC3D_PT_TextManagement, ABC3D_PT_FontCreation, ABC3D_PT_TextPropertiesPanel, ABC3D_OT_OpenAssetDirectory, ABC3D_OT_LoadInstalledFonts, ABC3D_OT_LoadFont, ABC3D_OT_AddDefaultMetrics, ABC3D_OT_RemoveMetrics, ABC3D_OT_AlignMetricsToActiveObject, ABC3D_OT_AlignMetrics, ABC3D_OT_TemporaryHelper, ABC3D_OT_RemoveText, ABC3D_OT_PlaceText, ABC3D_OT_InstallFont, ABC3D_OT_ToggleABC3DCollection, ABC3D_OT_SaveFontToFile, ABC3D_OT_CreateFontFromObjects, ABC3D_PT_RightPropertiesPanel, 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 detect_text(): scene = bpy.context.scene abc3d_data = scene.abc3d_data 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: t = abc3d_data.available_texts[linked_textobject] a = test_availability( o["font_name"], o["face_name"], o["text"]) butils.transfer_blender_object_to_text_properties(o, t) def load_used_glyphs(): print("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 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?"]) 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["maybe"]) > 0: for fp in a["filepaths"]: butils.load_font_from_filepath(fp, a["maybe"]) @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.update_available_fonts) butils.run_in_main_thread(bpy.ops.abc3d.load_installed_fonts) butils.run_in_main_thread(load_used_glyphs) 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) @persistent def on_depsgraph_update(scene, depsgraph): if not bpy.context.mode.startswith("EDIT"): 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': linked_textobject = u.id[f"{utils.prefix()}_linked_textobject"] if u.is_updated_geometry and len(scene.abc3d_data.available_texts) > linked_textobject: def later(): if not "lock_depsgraph_update_ntimes" in scene.abc3d_data \ or scene.abc3d_data["lock_depsgraph_update_ntimes"] == 0: print("******* not yet") butils.set_text_on_curve( 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) def register(): 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.Object.__del__ = lambda self: print(f"Bye {self.name}") print(f"REGISTER {bl_info['name']}") # auto start 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_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.load_installed_fonts) butils.run_in_main_thread(butils.update_available_fonts) butils.run_in_main_thread(butils.update_types) # bpy.ops.abc3d.load_installed_fonts() Font.name_to_glyph_d = Font.generate_name_to_glyph_d() 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_post: bpy.app.handlers.frame_change_post.remove(on_frame_changed) del bpy.types.Scene.abc3d_data print(f"UNREGISTER {bl_info['name']}") if __name__ == '__main__': register()