diff --git a/__init__.py b/__init__.py index c2ccfa1..fc97eb4 100644 --- a/__init__.py +++ b/__init__.py @@ -25,10 +25,12 @@ 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 @@ -173,25 +175,35 @@ class ABC3D_text_properties(bpy.types.PropertyGroup): 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.update_callback(context) + 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() - face_name: bpy.props.StringProperty() + 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=update_callback + update=glyphs_update_callback ) letter_spacing: bpy.props.FloatProperty( update=update_callback, @@ -233,9 +245,9 @@ class ABC3D_text_properties(bpy.types.PropertyGroup): #TODO: simply, merge, cut cut cut class ABC3D_data(bpy.types.PropertyGroup): - available_fonts: bpy.props.CollectionProperty(type=ABC3D_available_font, name="name of the collection property") + available_fonts: bpy.props.CollectionProperty(type=ABC3D_available_font, name="Available fonts") active_font_index: bpy.props.IntProperty() - available_texts: bpy.props.CollectionProperty(type=ABC3D_text_properties, name="") + 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 @@ -320,8 +332,33 @@ class ABC3D_PT_FontList(bpy.types.Panel): abc3d = scene.abc3d abc3d_data = scene.abc3d_data - layout.label(text="Loaded Fonts") + 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:") + 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 = box.row(); row.scale_y = scale_y + row.label(text=text) + n_rows = int(len(loaded_glyphs) / n) + box.row().label(text=f"Loaded Glyphs:", desription="") + 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 = box.row(); row.scale_y = scale_y + row.label(text=text) + class ABC3D_PT_TextPlacement(bpy.types.Panel): bl_label = "Place Text" @@ -403,13 +440,17 @@ class ABC3D_PT_TextManagement(bpy.types.Panel): for i in remove_list: if type(abc3d_data.available_texts[i].text_object) != type(None): - del mom[f"{utils.prefix()}_linked_textobject"] - del mom[f"{utils.prefix()}_font_name"] - del mom[f"{utils.prefix()}_face_name"] - del mom[f"{utils.prefix()}_font_size"] - del mom[f"{utils.prefix()}_letter_spacing"] - del mom[f"{utils.prefix()}_orientation"] - del mom[f"{utils.prefix()}_translation"] + 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") abc3d_data.available_texts.remove(i) for i, t in enumerate(abc3d_data.available_texts): @@ -422,7 +463,7 @@ class ABC3D_PT_TextManagement(bpy.types.Panel): if active_text_index != abc3d_data.active_text_index: abc3d_data.active_text_index = active_text_index - butils.run_in_main_thread(update) + # butils.run_in_main_thread(update) return True @@ -564,9 +605,18 @@ class ABC3D_OT_LoadInstalledFonts(bpy.types.Operator): 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.label(text="Loading font files can take a long time.") + 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) @@ -574,7 +624,10 @@ class ABC3D_OT_LoadInstalledFonts(bpy.types.Operator): def execute(self, context): scene = bpy.context.scene - butils.load_installed_fonts() + if self.load_into_memory: + butils.load_installed_fonts() + else: + butils.register_installed_fonts() butils.ShowMessageBox("Loading Fonts", 'INFO', "Updating Data Structures.") @@ -744,17 +797,23 @@ class ABC3D_OT_PlaceText(bpy.types.Operator): while text_id == tt.text_id: text_id = text_id + 1 t = abc3d_data.available_texts.add() - t.text_id = text_id - t.font_name = self.font_name - t.face_name = self.face_name + # 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_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.translation = self.translation - t.orientation = self.orientation - t.distribution_type = distribution_type + t['text'] = self.text + t['letter_spacing'] = self.letter_spacing + t['font_size'] = self.font_size + t['translation'] = self.translation + t['orientation'] = self.orientation + t['distribution_type'] = distribution_type + butils.prepare_text(t.font_name, + t.face_name, + t.text) + butils.set_text_on_curve(t) else: butils.ShowMessageBox( title="No object selected", @@ -1134,6 +1193,8 @@ class ABC3D_OT_Reporter(bpy.types.Operator): return {'FINISHED'} classes = ( + bimport.ImportGLTF2, + bimport.GetFontFacesInFile, ABC3D_addonPreferences, ABC3D_available_font, ABC3D_glyph_properties, @@ -1169,7 +1230,7 @@ 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(bpy.ops.abc3d.load_installed_fonts) def load_handler_unload(): if bpy.app.timers.is_registered(butils.execute_queued_functions): @@ -1180,10 +1241,6 @@ 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) - # for i, t in enumerate(bpy.context.scene.abc3d_data.available_texts): - # # TODO PERFORMANCE: only on demand - # # butils.set_text_on_curve(t) - # pass def register(): for cls in classes: diff --git a/bimport.py b/bimport.py new file mode 100644 index 0000000..789ae7b --- /dev/null +++ b/bimport.py @@ -0,0 +1,431 @@ +import bpy +from bpy.props import (StringProperty, + BoolProperty, + EnumProperty, + IntProperty, + FloatProperty, + CollectionProperty) +from bpy.types import Operator +from bpy_extras.io_utils import ImportHelper, ExportHelper +from io_scene_gltf2 import ConvertGLTF2_Base +from .common import Font + +# taken from blender_git/blender/scripts/addons/io_scene_gltf2/__init__.py + +def get_font_faces_in_file(filepath): + from io_scene_gltf2.io.imp.gltf2_io_gltf import glTFImporter, ImportError + + try: + import_settings = { 'import_user_extensions': [] } + gltf_importer = glTFImporter(filepath, import_settings) + gltf_importer.read() + gltf_importer.checks() + + out = [] + for node in gltf_importer.data.nodes: + if type(node.extras) != type(None) \ + and "glyph" in node.extras \ + and not ("type" in node.extras and node.extras["type"] is "metrics"): + out.append(node.extras) + return out + + except ImportError as e: + return None + +# taken from blender_git/blender/scripts/addons/io_scene_gltf2/__init__.py + +class GetFontFacesInFile(Operator, ImportHelper): + """Load a glTF 2.0 font and check which faces are in there""" + bl_idname = f"abc3d.check_font_gltf" + bl_label = 'Check glTF 2.0 Font' + bl_options = {'REGISTER', 'UNDO'} + + files: CollectionProperty( + name="File Path", + type=bpy.types.OperatorFileListElement, + ) + +# bpy.ops.abc3d.check_font_gltf(filepath="/home/jrkb/.config/blender/4.1/datafiles/abc3d/fonts/JRKB_LOL.glb") + found_fonts = [] + + def execute(self, context): + return self.check_gltf2(context) + + def check_gltf2(self, context): + import os + import sys + + if self.files: + # Multiple file check + ret = {'CANCELLED'} + dirname = os.path.dirname(self.filepath) + for file in self.files: + path = os.path.join(dirname, file.name) + if self.unit_check(path) == {'FINISHED'}: + ret = {'FINISHED'} + return ret + else: + # Single file check + return self.unit_check(self.filepath) + + def unit_check(self, filename): + self.found_fonts.append(["LOL","WHATEVER"]) + return {'FINISHED'} + +class ImportGLTF2(Operator, ConvertGLTF2_Base, ImportHelper): + """Load a glTF 2.0 font""" + bl_idname = f"abc3d.import_font_gltf" + bl_label = 'Import glTF 2.0 Font' + bl_options = {'REGISTER', 'UNDO'} + + filter_glob: StringProperty(default="*.glb;*.gltf", options={'HIDDEN'}) + + files: CollectionProperty( + name="File Path", + type=bpy.types.OperatorFileListElement, + ) + + loglevel: IntProperty( + name='Log Level', + description="Log Level") + + import_pack_images: BoolProperty( + name='Pack Images', + description='Pack all images into .blend file', + default=True + ) + + merge_vertices: BoolProperty( + name='Merge Vertices', + description=( + 'The glTF format requires discontinuous normals, UVs, and ' + 'other vertex attributes to be stored as separate vertices, ' + 'as required for rendering on typical graphics hardware. ' + 'This option attempts to combine co-located vertices where possible. ' + 'Currently cannot combine verts with different normals' + ), + default=False, + ) + + import_shading: EnumProperty( + name="Shading", + items=(("NORMALS", "Use Normal Data", ""), + ("FLAT", "Flat Shading", ""), + ("SMOOTH", "Smooth Shading", "")), + description="How normals are computed during import", + default="NORMALS") + + bone_heuristic: EnumProperty( + name="Bone Dir", + items=( + ("BLENDER", "Blender (best for import/export round trip)", + "Good for re-importing glTFs exported from Blender, " + "and re-exporting glTFs to glTFs after Blender editing. " + "Bone tips are placed on their local +Y axis (in glTF space)"), + ("TEMPERANCE", "Temperance (average)", + "Decent all-around strategy. " + "A bone with one child has its tip placed on the local axis " + "closest to its child"), + ("FORTUNE", "Fortune (may look better, less accurate)", + "Might look better than Temperance, but also might have errors. " + "A bone with one child has its tip placed at its child's root. " + "Non-uniform scalings may get messed up though, so beware"), + ), + description="Heuristic for placing bones. Tries to make bones pretty", + default="BLENDER", + ) + + guess_original_bind_pose: BoolProperty( + name='Guess Original Bind Pose', + description=( + 'Try to guess the original bind pose for skinned meshes from ' + 'the inverse bind matrices. ' + 'When off, use default/rest pose as bind pose' + ), + default=True, + ) + + import_webp_texture: BoolProperty( + name='Import WebP textures', + description=( + "If a texture exists in WebP format, " + "loads the WebP texture instead of the fallback PNG/JPEG one" + ), + default=False, + ) + + glyphs: StringProperty( + name='Import only these glyphs', + description=( + "Loading glyphs is expensive, if the meshes are huge" + "So we can filter all glyphs out that we do not want" + ), + default="A", + ) + + marker_property: StringProperty( + name="Mark imported objects with this custom property.", + default="font_import", + ) + + font_name: StringProperty( + name="If defined, only import this font", + default="", + ) + + face_name: StringProperty( + name="If defined, only import this font face", + default="", + ) + + def draw(self, context): + layout = self.layout + + layout.use_property_split = True + layout.use_property_decorate = False # No animation. + + layout.prop(self, 'import_pack_images') + layout.prop(self, 'merge_vertices') + layout.prop(self, 'import_shading') + layout.prop(self, 'guess_original_bind_pose') + layout.prop(self, 'bone_heuristic') + layout.prop(self, 'export_import_convert_lighting_mode') + layout.prop(self, 'import_webp_texture') + + def invoke(self, context, event): + import sys + preferences = bpy.context.preferences + for addon_name in preferences.addons.keys(): + try: + if hasattr(sys.modules[addon_name], 'glTF2ImportUserExtension') or hasattr(sys.modules[addon_name], 'glTF2ImportUserExtensions'): + importer_extension_panel_unregister_functors.append(sys.modules[addon_name].register_panel()) + except Exception: + pass + + self.has_active_importer_extensions = len(importer_extension_panel_unregister_functors) > 0 + return ImportHelper.invoke(self, context, event) + + def execute(self, context): + return self.import_gltf2(context) + + def import_gltf2(self, context): + import os + + self.set_debug_log() + import_settings = self.as_keywords() + + user_extensions = [] + + import sys + preferences = bpy.context.preferences + for addon_name in preferences.addons.keys(): + try: + module = sys.modules[addon_name] + except Exception: + continue + if hasattr(module, 'glTF2ImportUserExtension'): + extension_ctor = module.glTF2ImportUserExtension + user_extensions.append(extension_ctor()) + import_settings['import_user_extensions'] = user_extensions + + if self.files: + # Multiple file import + ret = {'CANCELLED'} + dirname = os.path.dirname(self.filepath) + for file in self.files: + path = os.path.join(dirname, file.name) + if self.unit_import(path, import_settings) == {'FINISHED'}: + ret = {'FINISHED'} + return ret + else: + # Single file import + return self.unit_import(self.filepath, import_settings) + + def unit_import(self, filename, import_settings): + import time + from io_scene_gltf2.io.imp.gltf2_io_gltf import glTFImporter, ImportError + from io_scene_gltf2.blender.imp.gltf2_blender_gltf import BlenderGlTF + from io_scene_gltf2.blender.imp.gltf2_blender_vnode import VNode, compute_vnodes + from io_scene_gltf2.blender.com.gltf2_blender_extras import set_extras + from io_scene_gltf2.blender.imp.gltf2_blender_node import BlenderNode + + try: + gltf = glTFImporter(filename, import_settings) + gltf.read() + gltf.checks() + + # start filtering glyphs like this: + # - collect indices of nodes that contain our glyphs + # - collect indices of their meshes + # - collect the node's parent tree + # - use these indices to create new lists of nodes and meshes + # - update the scene tree to contain only our nodes + # - replace the node and mesh list with ours + + # indices of meshes to keep + mesh_indices = [] + # indices of nodes to keep + node_indices = [] + + # convenience function to add a node to the indices + def add_node(node, recursive=True): + node_index = gltf.data.nodes.index(node) + if node_index not in node_indices: + node_indices.append(node_index) + if type(node.mesh) != type(None) and node.mesh >= 0: + mesh_index = node.mesh + if mesh_index not in mesh_indices: + mesh_indices.append(mesh_index) + if recursive and type(node.children) != type(None): + for c in node.children: + child = gltf.data.nodes[c] + add_node(child) + + # convenience function to add a mesh to the indices + def add_parent_node(node, recursive=True): + index = gltf.data.nodes.index(node) + for parent in gltf.data.nodes: + if type(parent.children) != type(None) and index in parent.children: + add_node(parent, False) + if recursive: + add_parent_node(parent) + + # populate our node_indices and mesh_indices + # by iterating through the nodes and check if they are + # indeed representing a glyph we want + for node in gltf.data.nodes: + # :-O woah + if type(node.extras) != type(None) \ + and "glyph" in node.extras \ + and (node.extras["glyph"] in self.glyphs \ + or len(self.glyphs) == 0) \ + and (self.font_name == "" or \ + ( "font_name" in node.extras \ + and (node.extras["font_name"] in self.font_name \ + or len(self.glyphs) == 0))) \ + and (self.face_name == "" or \ + ( "face_name" in node.extras \ + and (node.extras["face_name"] in self.face_name \ + or len(self.glyphs) == 0))): + # if there is a match, add the node incl children .. + add_node(node) + # .. and their parents recursively + add_parent_node(node) + + # in the end we need the objects, not the indices + # so let's prepare empy lists + meshes = [] + nodes = [] + + # the indices will be off, as we have fewer elements + # so let's have a lookup table + mesh_index_table = {} + node_index_table = {} + + # first, add all meshes and fill in lookup table + for mesh_index, mesh in enumerate(gltf.data.meshes): + if mesh_index in mesh_indices: + meshes.append(mesh) + mesh_index_table[mesh_index] = len(meshes) - 1 + + # second, add all nodes and fill in lookup table + # nodes also refer to their meshes + for node_index, node in enumerate(gltf.data.nodes): + if node_index in node_indices: + if type(node.mesh) != type(None): + node.mesh = mesh_index_table[node.mesh] + nodes.append(node) + node_index_table[node_index] = len(nodes) - 1 + + # the indices to children are messed up. + # some children are lost :( + # and some have different indices + for node in nodes: + if type(node.children) != type(None): + children = [] # brand new children + for i, c in enumerate(node.children): + # check if children are lost + if c in node_indices: + children.append(node_index_table[c]) + # now replace old children with the new, however + # if we don't have children, we don't even need a list! + node.children = None if len(children) == 0 else children + + # last step, kick nodes out of the scene tree if they're lost + for s in gltf.data.scenes: + scene_nodes = [] + for n in s.nodes: + if n in node_indices: + scene_nodes.append(node_index_table[n]) + s.nodes = scene_nodes + + # very last step, replace nodes and meshes + gltf.data.nodes = nodes + gltf.data.meshes = meshes + + # that's fucking it, we're done! + # hand over back to default blender behaviour :-) + # or.. not! blender will do some funny scene stuff + # which we don't want. + # so let's do it quick + + print("Data are loaded, start creating Blender stuff") + + start_time = time.time() + # first, convert gltf to blender + BlenderGlTF.set_convert_functions(gltf) + # compute things + BlenderGlTF.pre_compute(gltf) + compute_vnodes(gltf) + + # apparently we need a scene, because + # when creating the objects, it will link the objects here + gltf.blender_scene = bpy.context.scene.name + + def create_blender_object(gltf, vi, nodes): + vnode = gltf.vnodes[vi] + if vnode.type == VNode.Object: + if vnode.parent is not None: + if not hasattr(gltf.vnodes[vnode.parent], + "blender_object"): + create_blender_object(gltf, + vnode.parent, + nodes) + if not hasattr(vnode, + "blender_object"): + obj = BlenderNode.create_object(gltf, vi) + obj["font_import"] = True + n_vars = vars(nodes[vi]) + if "extras" in n_vars: + set_extras(obj, n_vars["extras"]) + if "glyph" in n_vars["extras"] and \ + not ("type" in n_vars["extras"] and \ + n_vars["extras"]["type"] == "metrics"): + obj["type"] = "glyph" + + for vi, vnode in gltf.vnodes.items(): + create_blender_object(gltf, vi, nodes) + + elapsed_s = "{:.2f}s".format(time.time() - start_time) + print("font import gltf finished in " + elapsed_s) + + gltf.log.removeHandler(gltf.log_handler) + + return {'FINISHED'} + + except ImportError as e: + self.report({'ERROR'}, e.args[0]) + return {'CANCELLED'} + + def set_debug_log(self): + import logging + if bpy.app.debug_value == 0: + self.loglevel = logging.CRITICAL + elif bpy.app.debug_value == 1: + self.loglevel = logging.ERROR + elif bpy.app.debug_value == 2: + self.loglevel = logging.WARNING + elif bpy.app.debug_value == 3: + self.loglevel = logging.INFO + else: + self.loglevel = logging.NOTSET diff --git a/butils.py b/butils.py index 213e30a..6825d89 100644 --- a/butils.py +++ b/butils.py @@ -314,7 +314,6 @@ def find_font_face_object(font_obj, face_name): return None def move_in_fontcollection(obj, fontcollection, allow_duplicates=False): - # parent nesting structure # the font object font_obj = find_font_object(fontcollection, @@ -377,80 +376,104 @@ def move_in_fontcollection(obj, fontcollection, allow_duplicates=False): return obj -def load_font_from_filepath(filepath): +def register_font_from_filepath(filepath): + from .bimport import get_font_faces_in_file + + availables = get_font_faces_in_file(filepath) + + fonts = {} + for a in availables: + font_name = a["font_name"] + face_name = a["face_name"] + glyph = a["glyph"] + if not font_name in fonts: + fonts[font_name] = {} + if not face_name in fonts[font_name]: + fonts[font_name][face_name] = [] + fonts[font_name][face_name].append(glyph) + for font_name in fonts: + for face_name in fonts[font_name]: + Font.register_font(font_name, + face_name, + fonts[font_name][face_name], + filepath) + +def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""): if not filepath.endswith(".glb") and not filepath.endswith(".gltf"): ShowMessageBox(f"{bl_info['name']} Font loading error", 'ERROR', f"Filepath({filepath}) is not a *.glb or *.gltf file") return False - abc3d_data = bpy.context.scene.abc3d_data - allObjectsBefore = [] - for ob in bpy.data.objects: - allObjectsBefore.append(ob.name) - - bpy.ops.import_scene.gltf(filepath=filepath) + marker_property = "font_import" + bpy.ops.abc3d.import_font_gltf(filepath=filepath, + glyphs=glyphs, + marker_property=marker_property, + font_name=font_name, + face_name=face_name) fontcollection = bpy.data.collections.get("ABC3D") if fontcollection is None: fontcollection = bpy.data.collections.new("ABC3D") + modified_font_faces = [] + all_glyph_os = [] remove_list = [] all_objects = [] - for o in bpy.data.objects: - all_objects.append(o) - for o in all_objects: - o_exists = True - try: - o, o.name - except ReferenceError as e: - o_exists = False - if o_exists and o.name not in allObjectsBefore: - # must be new - if ("glyph" in o.keys() - and "face_name" in o.keys() - and "font_name" in o.keys() - and not ("type" in o.keys() and o["type"] == "metrics") - and not is_metrics_object(o) - ): - glyph_id = o["glyph"] - font_name = o["font_name"] - face_name = o["face_name"] - # ShowMessageBox("Loading Font", "INFO", f"adding glyph {glyph_id} for {font_name} {face_name}") - print(f"adding glyph {glyph_id} for {font_name} {face_name}") - glyph_obj = move_in_fontcollection( - o, - fontcollection) - Font.add_glyph( - font_name, - face_name, - glyph_id, - glyph_obj) - for c in o.children: - if is_metrics_object(c): - add_metrics_obj_from_bound_box(glyph_obj, - bound_box_as_array(c.bound_box)) - if glyph_obj != o: - remove_list.append(o) - - # found = False - # for f in abc3d_data.available_fonts.values(): - # print(f"has in availables {f.font_name} {f.face_name}") - # 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 - # print(f"{__name__} added {font_name} {face_name}") - elif o_exists: + for o in bpy.context.scene.objects: + if marker_property in o: + if "type" in o and o["type"] == "glyph": + all_glyph_os.append(o) + else: remove_list.append(o) - for o in remove_list: - try: - bpy.data.objects.remove(o, do_unlink=True) - except ReferenceError as e: - print(f"{__name__} could not remove object, because it doesn't exist") - print(f"{__name__}: loaded font from {filepath}") + + for o in all_glyph_os: + glyph_id = o["glyph"] + font_name = o["font_name"] + face_name = o["face_name"] + del o[marker_property] + + glyph_obj = move_in_fontcollection( + o, + fontcollection) + Font.add_glyph( + font_name, + face_name, + glyph_id, + glyph_obj) + for c in o.children: + if is_metrics_object(c): + add_metrics_obj_from_bound_box(glyph_obj, + bound_box_as_array(c.bound_box)) + modified_font_faces.append({"font_name": font_name, + "face_name": face_name}) + + if glyph_obj != o: + remove_list.append(o) + + for mff in modified_font_faces: + glyphs = [] + face = Font.fonts[mff["font_name"]].faces[mff["face_name"]] + # iterate glyphs + for g in face.glyphs: + # iterate alternates + for glyph in face.glyphs[g]: + glyphs.append(glyph) + if len(glyphs) > 0: + add_default_metrics_to_objects(glyphs) + # calculate unit factor + h = get_glyph_height(glyphs[0]) + if h != 0: + face.unit_factor = 1 / h + update_available_fonts() + remove_list = [] + for o in bpy.context.scene.collection.all_objects: + if not o.name in fontcollection.all_objects: + if marker_property in o and o[marker_property] == True: + remove_list.append(o) + + simply_delete_objects(remove_list) + + # completely_delete_objects(remove_list) def update_available_fonts(): abc3d_data = bpy.context.scene.abc3d_data @@ -491,11 +514,24 @@ def load_installed_fonts(): if file.endswith(".glb") or file.endswith(".gltf"): font_path = os.path.join(font_dir, file) # ShowMessageBox("Loading Font", "INFO", f"loading font from {font_path}") - print(f"loading font from {font_path}") - for f in bpy.context.scene.abc3d_data.available_fonts.values(): - print(f"available font: {f.font_name} {f.face_name}") + # print(f"loading font from {font_path}") + # for f in bpy.context.scene.abc3d_data.available_fonts.values(): + # print(f"available font: {f.font_name} {f.face_name}") + register_font_from_filepath(font_path) load_font_from_filepath(font_path) +def register_installed_fonts(): + preferences = getPreferences(bpy.context) + font_dir = f"{preferences.assets_dir}/fonts" + for file in os.listdir(font_dir): + if file.endswith(".glb") or file.endswith(".gltf"): + font_path = os.path.join(font_dir, file) + # ShowMessageBox("Loading Font", "INFO", f"loading font from {font_path}") + # print(f"loading font from {font_path}") + # for f in bpy.context.scene.abc3d_data.available_fonts.values(): + # print(f"available font: {f.font_name} {f.face_name}") + register_font_from_filepath(font_path) + def ShowMessageBox(title = "Message Box", icon = 'INFO', message=""): """Show a simple message box @@ -531,12 +567,15 @@ def ShowMessageBox(title = "Message Box", icon = 'INFO', message=""): self.layout.label(text=n) bpy.context.window_manager.popup_menu(draw, title = title, icon = icon) -def completely_delete_objects(objs): +def simply_delete_objects(objs): context_override = bpy.context.copy() context_override["selected_objects"] = list(objs) with bpy.context.temp_override(**context_override): bpy.ops.object.delete() +def completely_delete_objects(objs): + simply_delete_objects(objs) + # remove deleted objects # this is necessary for g in objs: @@ -578,7 +617,23 @@ def get_glyph_advance(glyph_obj): return abs(c.bound_box[4][0] - c.bound_box[0][0]) return abs(glyph_obj.bound_box[4][0] - glyph_obj.bound_box[0][0]) -def set_text_on_curve(text_properties): +def get_glyph_height(glyph_obj): + for c in glyph_obj.children: + if is_metrics_object(c): + return abs(c.bound_box[0][1] - c.bound_box[3][1]) + return abs(glyph_obj.bound_box[0][1] - glyph_obj.bound_box[3][1]) + +def prepare_text(font_name, face_name, text): + loaded, missing, loadable, files = Font.test_glyphs_availability( + font_name, + face_name, + text) + if len(loadable) > 0: + for filepath in files: + load_font_from_filepath(filepath, loadable, font_name, face_name) + return True + +def set_text_on_curve(text_properties, recursive=True): # starttime = time.perf_counter_ns() mom = text_properties.text_object if mom.type != "CURVE": @@ -605,6 +660,22 @@ def set_text_on_curve(text_properties): # if we regenerate.... delete objects if regenerate: + # loaded, missing, maybe, files = Font.test_glyphs_availability( + # text_properties.font_name, + # text_properties.face_name, + # text_properties.text) + # if len(maybe) > 0 and recursive: + # print(f"doing the thing {len(files)} times") + # for filepath in files: + # def loader(): + # set_text_on_curve(text_properties, False) + # print(f"loading font from filepath {filepath} {maybe}") + # load_font_from_filepath(filepath, maybe) + # print(f"font: {text_properties.font_name} face: {text_properties.face_name}") + # print("text",text_properties.text) + # text_properties.font_size = text_properties.font_size + # # run_in_main_thread(loader) + # return completely_delete_objects(glyph_objects) # context_override = bpy.context.copy() # context_override["selected_objects"] = list(glyph_objects) @@ -696,7 +767,8 @@ def set_text_on_curve(text_properties): ob.rotation_quaternion = q # ob.rotation_quaternion = (mom.matrix_world @ q.to_matrix().to_4x4()).to_quaternion() - scalor = 0.001 * text_properties.font_size + face = Font.fonts[text_properties.font_name].faces[text_properties.face_name] + scalor = face.unit_factor * text_properties.font_size glyph_advance = get_glyph_advance(glyph) * scalor + text_properties.letter_spacing @@ -929,6 +1001,13 @@ def get_metrics_bound_box(bb, bb_uebermetrics): metrics[7][0] = bb[7][0] return metrics +def get_metrics_object(o): + if is_glyph(o): + for c in o.children: + if is_metrics_object(c): + return c + return None + def add_default_metrics_to_objects(objects=None, overwrite_existing=False): if type(objects) == type(None): objects=bpy.context.selected_objects diff --git a/common/Font.py b/common/Font.py index b8a595e..8e467dd 100644 --- a/common/Font.py +++ b/common/Font.py @@ -91,10 +91,24 @@ class FontFace: :param glyphs: dictionary of glyphs, defaults to ``{}`` :type glyphs: dict, optional + :param loaded_glyphs: glyphs currently loaded + :type loaded_glyphs: List[str], optional + :param missing_glyphs: glyphs not present in the fontfile + :type missing_glyphs: List[str], optional + :param filenames: from which file is this face + :type filenames: List[str] """ - def __init__(self, glyphs = {}): + def __init__(self, + glyphs = {}): self.glyphs = glyphs - + # lists have to be initialized in __init__ + # to be attributes per instance. + # otherwise they are static class attributes + self.loaded_glyphs = [] + self.missing_glyphs = [] + self.glyphs_in_fontfile = [] + self.filepaths = [] + self.unit_factor = 1.0 class Font: """Font holds the faces and various metadata for a font @@ -108,6 +122,19 @@ class Font: # TODO: better class structure? # TODO: get fonts and faces directly + +def register_font(font_name, face_name, glyphs_in_fontfile, filepath): + if not fonts.keys().__contains__(font_name): + fonts[font_name] = Font({}) + if fonts[font_name].faces.get(face_name) == None: + fonts[font_name].faces[face_name] = FontFace({}) + fonts[font_name].faces[face_name].glyphs_in_fontfile = glyphs_in_fontfile + else: + fonts[font_name].faces[face_name].glyphs_in_fontfile = \ + list(set(fonts[font_name].faces[face_name].glyphs_in_fontfile + glyphs_in_fontfile)) + if filepath not in fonts[font_name].faces[face_name].filepaths: + fonts[font_name].faces[face_name].filepaths.append(filepath) + def add_glyph(font_name, face_name, glyph_id, glyph_object): """ add_glyph adds a glyph to a FontFace @@ -125,15 +152,14 @@ def add_glyph(font_name, face_name, glyph_id, glyph_object): if not fonts.keys().__contains__(font_name): fonts[font_name] = Font({}) - # print("is it has been added", fonts.keys()) if fonts[font_name].faces.get(face_name) == None: fonts[font_name].faces[face_name] = FontFace({}) - # print("is it has been added faces", fonts[font_name].faces[face_name]) if fonts[font_name].faces[face_name].glyphs.get(glyph_id) == None: fonts[font_name].faces[face_name].glyphs[glyph_id] = [] - # print("is it has been added glyph", fonts[font_name].faces[face_name].glyphs[glyph_id]) fonts[font_name].faces[face_name].glyphs.get(glyph_id).append(glyph_object) + if glyph_id not in fonts[font_name].faces[face_name].loaded_glyphs: + fonts[font_name].faces[face_name].loaded_glyphs.append(glyph_id) def get_glyph(font_name, face_name, glyph_id, alternate=0): """ add_glyph adds a glyph to a FontFace @@ -149,7 +175,7 @@ def get_glyph(font_name, face_name, glyph_id, alternate=0): :return: returns the glyph object, or ``None`` if it does not exist :rtype: `Object` """ - # print(fonts) + if not fonts.keys().__contains__(font_name): print(f"ABC3D::get_glyph: font name({font_name}) not found") print(fonts.keys()) @@ -164,10 +190,32 @@ def get_glyph(font_name, face_name, glyph_id, alternate=0): glyphs_for_id = face.glyphs.get(glyph_id) if glyphs_for_id == None or len(glyphs_for_id) <= alternate: print(f"ABC3D::get_glyph: font({font_name}) face({face_name}) glyph({glyph_id})[{alternate}] not found") + if glyph_id not in fonts[font_name].faces[face_name].missing_glyphs: + fonts[font_name].faces[face_name].missing_glyphs.append(glyph_id) return None return fonts[font_name].faces[face_name].glyphs.get(glyph_id)[alternate] +def test_glyphs_availability(font_name, face_name, text): + # maybe there is NOTHING yet + if not fonts.keys().__contains__(font_name) or \ + fonts[font_name].faces.get(face_name) == None: + return "", "", text # , , + + loaded = [] + missing = [] + maybe = [] + for c in text: + if c in fonts[font_name].faces[face_name].loaded_glyphs: + loaded.append(c) + elif c in fonts[font_name].faces[face_name].glyphs_in_fontfile: + maybe.append(c) + else: + if c not in fonts[font_name].faces[face_name].missing_glyphs: + fonts[font_name].faces[face_name].missing_glyphs.append(c) + missing.append(c) + return ''.join(loaded), ''.join(missing), ''.join(maybe), fonts[font_name].faces[face_name].filepaths + def get_loaded_fonts(): return fonts.keys()