# SPDX-License-Identifier: GPL-2.0-only """ A 3D font helper """ bl_info = { "name": "ABC3D", "author": "Jakob Schlötter, Studio Pointer*", "version": (0, 0, 1), "blender": (4, 1, 0), "location": "VIEW3D", "description": "Does ABC3D stuff", "category": "Typography", } # make sure that modules are reloadable # when registering # handy for development # first import dependencies for the method import importlib # then import dependencies for our addon if "bpy" in locals(): importlib.reload(Font) importlib.reload(utils) importlib.reload(butils) importlib.reload(bimport) else: from .common import Font from .common import utils from . import butils from . import bimport import bpy import math import mathutils import io import functools from bpy.types import Panel from bpy.app.handlers import persistent from random import uniform import time import datetime import os import re def getPreferences(context): preferences = context.preferences return preferences.addons[__name__].preferences class ABC3D_addonPreferences(bpy.types.AddonPreferences): """ABC3D Addon Preferences These are the preferences at Edit/Preferences/Add-ons""" bl_idname = __name__ 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") 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): layout.label(text=f"{index}: {item.font_name} {item.face_name}") # avoids renaming the item by accident 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)) split.label(text=f"{item.text}") # avoids renaming the item by accident 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__}.load_installed_fonts", text="load installed fonts", icon=icon) 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): if type(bpy.context.active_object) != type(None):# and bpy.context.object.select_get(): 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") # crashes on Mac OS? # 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): abc3d_data = context.scene.abc3d_data self.font_path = abc3d_data.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 if not os.path.exists(self.font_path): butils.ShowMessageBox( title=f"{__name__} Warning", icon="ERROR", message=[ f"Could not install font.", f"We believe the font path ({self.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(self.font_path) target = os.path.join(preferences.assets_dir, "fonts", filename) import shutil os.makedirs(os.path.dirname(target), exist_ok=True) shutil.copyfile(self.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(self.font_path) if self.load_into_memory: butils.load_font_from_filepath(self.font_path) return {'FINISHED'} 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, export_format='GLB', # GLB or GLTF_SEPARATE (also change filepath) 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_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) @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) 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) def register(): for cls in classes: 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) 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) # bpy.ops.abc3d.load_installed_fonts() Font.name_to_glyph_d = Font.generate_name_to_glyph_d() def unregister(): for cls in 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()