From 2dcd4e7a2c61de015889d8e3291752fc115d193d Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sat, 31 May 2025 16:13:16 +0200 Subject: [PATCH] [feature] unload glyphs --- __init__.py | 234 +++++++++++--------- butils.py | 576 ++++++++++++++++++++++++++++++++++--------------- common/Font.py | 22 +- 3 files changed, 552 insertions(+), 280 deletions(-) diff --git a/__init__.py b/__init__.py index 55ef467..6e071a0 100644 --- a/__init__.py +++ b/__init__.py @@ -35,6 +35,7 @@ if "Font" in locals(): importlib.reload(bimport) importlib.reload(addon_updater_ops) + def getPreferences(context): preferences = context.preferences return preferences.addons[__name__].preferences @@ -137,7 +138,7 @@ class ABC3D_glyph_properties(bpy.types.PropertyGroup): def update_callback(self, context): if self.text_id >= 0: # butils.set_text_on_curve( - # context.scene.abc3d_data.available_texts[self.text_id] + # context.scene.abc3d_data.available_texts[self.text_id] # ) t = butils.get_text_properties(self.text_id) if t is not None: @@ -145,8 +146,8 @@ class ABC3D_glyph_properties(bpy.types.PropertyGroup): glyph_id: bpy.props.StringProperty(maxlen=1) text_id: bpy.props.IntProperty( - default=-1, - ) + default=-1, + ) alternate: bpy.props.IntProperty( default=-1, update=update_callback, @@ -158,6 +159,7 @@ class ABC3D_glyph_properties(bpy.types.PropertyGroup): update=update_callback, ) + class ABC3D_text_properties(bpy.types.PropertyGroup): def font_items_callback(self, context): items = [] @@ -165,21 +167,6 @@ class ABC3D_text_properties(bpy.types.PropertyGroup): 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 - 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 not isinstance(self.font_name, None) and not isinstance(self.face_name, None): - return 0 # f"{self.font_name} {self.face_name}" - else: - return 0 # "" - def glyphs_update_callback(self, context): butils.prepare_text(self.font_name, self.face_name, self.text) butils.set_text_on_curve(self, can_regenerate=True) @@ -283,13 +270,16 @@ class ABC3D_data(bpy.types.PropertyGroup): def active_text_index_update(self, context): if self.active_text_index != -1: - text_properties = butils.get_text_properties(self.active_text_index, context.scene) + text_properties = butils.get_text_properties( + self.active_text_index, context.scene + ) if text_properties is not None: o = text_properties.text_object # active_text_index changed. so let's update the selection # check if it is already selected # or perhaps one of the glyphs - if (o is not None + if ( + o is not None and not o.select_get() and not len([c for c in o.children if c.select_get()]) > 0 ): @@ -397,7 +387,7 @@ class ABC3D_PT_FontList(bpy.types.Panel): available_font = abc3d_data.available_fonts[abc3d_data.active_font_index] font_name = available_font.font_name face_name = available_font.face_name - face : Font.FontFace = Font.get_font_face(font_name, face_name) + face: Font.FontFace = Font.get_font_face(font_name, face_name) if face is not None: available_glyphs = face.glyphs_in_fontfile loaded_glyphs = sorted(face.loaded_glyphs) @@ -455,10 +445,7 @@ class ABC3D_PT_TextPlacement(bpy.types.Panel): @classmethod def poll(self, context): - if ( - context.active_object is not None - and context.active_object.type == "CURVE" - ): + if context.active_object is not None and context.active_object.type == "CURVE": self.can_place = True else: self.can_place = False @@ -649,6 +636,7 @@ class ABC3D_PG_FontCreation(bpy.types.PropertyGroup): update=naming_glyph_id_update_callback, ) + class ABC3D_OT_NamingHelper(bpy.types.Operator): bl_label = "Font Creation Naming Helper Apply To Active Object" bl_idname = f"{__name__}.apply_naming_helper" @@ -686,7 +674,10 @@ class ABC3D_PT_NamingHelper(bpy.types.Panel): box.row().prop(abc3d_font_creation, "face_name") box.label(text="Glyph Output Name") box.row().prop(abc3d_font_creation, "naming_glyph_name") - box.row().operator(f"{__name__}.apply_naming_helper", text="Apply name to active object") + box.row().operator( + f"{__name__}.apply_naming_helper", text="Apply name to active object" + ) + class ABC3D_PT_FontCreation(bpy.types.Panel): bl_label = "Font Creation" @@ -725,7 +716,10 @@ class ABC3D_PT_FontCreation(bpy.types.Panel): f"{__name__}.temporaryhelper", text="Debug Function Do Not Use" ) box.label(text="origin points") - box.row().operator(f"{__name__}.align_origins_to_active_object", text="Align origins to Active Object") + box.row().operator( + f"{__name__}.align_origins_to_active_object", + text="Align origins to Active Object", + ) # box.row().operator(f"{__name__}.align_origins_to_metrics", text="Align origins to Metrics (left)") # box.row().operator(f"{__name__}.fix_objects_metrics_origins", text="Fix objects metrics origins") @@ -749,27 +743,36 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): return bpy.context.scene.abc3d_data.available_texts[text_index] else: for t in bpy.context.scene.abc3d_data.available_texts: - if butils.is_or_has_parent(bpy.context.active_object, t.text_object, max_depth=4): + if butils.is_or_has_parent( + bpy.context.active_object, t.text_object, max_depth=4 + ): return t return None def get_active_glyph_properties(self): a_o = bpy.context.active_object if a_o is not None: - if (f"{utils.prefix()}_text_id" in a_o - and f"{utils.prefix()}_glyph_index" in a_o): + if ( + f"{utils.prefix()}_text_id" in a_o + and f"{utils.prefix()}_glyph_index" in a_o + ): text_index = a_o[f"{utils.prefix()}_text_id"] glyph_index = a_o[f"{utils.prefix()}_glyph_index"] - return bpy.context.scene.abc3d_data.available_texts[text_index].glyphs[glyph_index] + return bpy.context.scene.abc3d_data.available_texts[text_index].glyphs[ + glyph_index + ] else: for t in bpy.context.scene.abc3d_data.available_texts: - if butils.is_or_has_parent(a_o, t.text_object, if_is_parent=False, max_depth=4): + if butils.is_or_has_parent( + a_o, t.text_object, if_is_parent=False, max_depth=4 + ): for g in t.glyphs: - if butils.is_or_has_parent(a_o, g.glyph_object, max_depth=4): + if butils.is_or_has_parent( + a_o, g.glyph_object, max_depth=4 + ): return g return None - # def font_items_callback(self, context): # items = [] # fonts = Font.get_loaded_fonts_and_faces() @@ -840,7 +843,6 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): box.row().prop(glyph_props, "letter_spacing") - class ABC3D_OT_InstallFont(bpy.types.Operator): """Install or load Fontfile from path above. (Format must be *.glb or *.gltf)""" @@ -1024,9 +1026,16 @@ class ABC3D_OT_LoadFont(bpy.types.Operator): face_name: bpy.props.StringProperty() def execute(self, context): - face : Font.FontFace = Font.get_font_face(self.font_name, self.face_name) + face: Font.FontFace = Font.get_font_face(self.font_name, self.face_name) if face is None: - butils.ShowMessageBox(f"{utils.prefix()} Load Font", icon="ERROR", message=["Could not load font, sorry!", f"{self.font_name=} {self.face_name=}"]) + butils.ShowMessageBox( + f"{utils.prefix()} Load Font", + icon="ERROR", + message=[ + "Could not load font, sorry!", + f"{self.font_name=} {self.face_name=}", + ], + ) return {"CANCELLED"} filepaths = face.filepaths for f in filepaths: @@ -1090,6 +1099,7 @@ class ABC3D_OT_AlignMetrics(bpy.types.Operator): butils.align_metrics_of_objects(objects) return {"FINISHED"} + class ABC3D_OT_AlignOriginsToActiveObject(bpy.types.Operator): """Align origins of selected objects to origin of active object on one axis.""" @@ -1097,65 +1107,67 @@ class ABC3D_OT_AlignOriginsToActiveObject(bpy.types.Operator): bl_label = "Align origins to Active Object" bl_options = {"REGISTER", "UNDO"} - enum_axis = (('0','X',''),('1','Y',''),('2','Z','')) - axis: bpy.props.EnumProperty(items = enum_axis, default='2') + enum_axis = (("0", "X", ""), ("1", "Y", ""), ("2", "Z", "")) + axis: bpy.props.EnumProperty(items=enum_axis, default="2") def execute(self, context): objects = bpy.context.selected_objects butils.align_origins_to_active_object(objects, int(self.axis)) return {"FINISHED"} + # class ABC3D_OT_AlignOriginsToMetrics(bpy.types.Operator): - # """Align origins of selected objects to their metrics left border. +# """Align origins of selected objects to their metrics left border. - # Be aware that shifting the origin will also shift the pivot point around which an object rotates. +# Be aware that shifting the origin will also shift the pivot point around which an object rotates. - # If an object does not have metrics, it will be ignored.""" +# If an object does not have metrics, it will be ignored.""" - # bl_idname = f"{__name__}.align_origins_to_metrics" - # bl_label = "Align origins to metrics metrics" - # bl_options = {"REGISTER", "UNDO"} +# bl_idname = f"{__name__}.align_origins_to_metrics" +# bl_label = "Align origins to metrics metrics" +# bl_options = {"REGISTER", "UNDO"} - # ignore_warning: bpy.props.BoolProperty( - # name="Do not warn in the future", - # description="Do not warn in the future", - # default=False, - # ) +# ignore_warning: bpy.props.BoolProperty( +# name="Do not warn in the future", +# description="Do not warn in the future", +# default=False, +# ) - # def draw(self, context): - # layout = self.layout - # layout.row().label(text="Warning!") - # layout.row().label(text="This also shifts the pivot point around which the glyph rotates.") - # layout.row().label(text="This may not be what you want.") - # layout.row().label(text="Glyph advance derives from metrics boundaries, not origin points.") - # layout.row().label(text="If you are sure about what you're doing, please continue.") - # layout.row().prop(self, "ignore_warning") +# def draw(self, context): +# layout = self.layout +# layout.row().label(text="Warning!") +# layout.row().label(text="This also shifts the pivot point around which the glyph rotates.") +# layout.row().label(text="This may not be what you want.") +# layout.row().label(text="Glyph advance derives from metrics boundaries, not origin points.") +# layout.row().label(text="If you are sure about what you're doing, please continue.") +# layout.row().prop(self, "ignore_warning") - # def invoke(self, context, event): - # if not self.ignore_warning: - # wm = context.window_manager - # return wm.invoke_props_dialog(self) - # return self.execute(context) +# def invoke(self, context, event): +# if not self.ignore_warning: +# wm = context.window_manager +# return wm.invoke_props_dialog(self) +# return self.execute(context) - # def execute(self, context): - # objects = bpy.context.selected_objects - # butils.align_origins_to_metrics(objects) - # butils.fix_objects_metrics_origins(objects) - # return {"FINISHED"} +# def execute(self, context): +# objects = bpy.context.selected_objects +# butils.align_origins_to_metrics(objects) +# butils.fix_objects_metrics_origins(objects) +# return {"FINISHED"} # class ABC3D_OT_FixObjectsMetricsOrigins(bpy.types.Operator): - # """Align metrics origins of selected objects to their metrics bounding box. +# """Align metrics origins of selected objects to their metrics bounding box. - # If an object does not have metrics, it will be ignored.""" +# If an object does not have metrics, it will be ignored.""" - # bl_idname = f"{__name__}.fix_objects_metrics_origins" - # bl_label = "Fix metrics origin of all selected objects" - # bl_options = {"REGISTER", "UNDO"} +# bl_idname = f"{__name__}.fix_objects_metrics_origins" +# bl_label = "Fix metrics origin of all selected objects" +# bl_options = {"REGISTER", "UNDO"} + +# def execute(self, context): +# objects = bpy.context.selected_objects +# butils.fix_objects_metrics_origins(objects) +# return {"FINISHED"} - # def execute(self, context): - # objects = bpy.context.selected_objects - # butils.fix_objects_metrics_origins(objects) - # return {"FINISHED"} class ABC3D_OT_TemporaryHelper(bpy.types.Operator): """Temporary Helper ABC3D\nThis could do anything.\nIt's just there to make random functions available for testing.""" @@ -1223,6 +1235,7 @@ class ABC3D_OT_RemoveText(bpy.types.Operator): mom = abc3d_data.available_texts[i].text_object if self.remove_custom_properties: + def delif(o, p): if p in o: del o[p] @@ -1372,7 +1385,8 @@ class ABC3D_OT_PlaceText(bpy.types.Operator): class ABC3D_OT_ToggleABC3DCollection(bpy.types.Operator): """Toggle ABC3D Collection. - This will show the Fonts and Glyphs currently loaded by ABC3D. Useful for font creation, debugging and inspection.""" + This will show the Fonts and Glyphs currently loaded by ABC3D. Useful for font creation, debugging and inspection. + """ bl_idname = f"{__name__}.toggle_abc3d_collection" bl_label = "Toggle Collection visibility" @@ -1404,8 +1418,8 @@ class ABC3D_OT_SaveFontToFile(bpy.types.Operator): bl_label = "Save Font" bl_options = {"REGISTER", "UNDO"} - can_execute : bpy.props.BoolProperty(default=True) - create_output_directory : bpy.props.BoolProperty(default=False) + can_execute: bpy.props.BoolProperty(default=True) + create_output_directory: bpy.props.BoolProperty(default=False) def invoke(self, context, event): wm = context.window_manager @@ -1430,7 +1444,7 @@ class ABC3D_OT_SaveFontToFile(bpy.types.Operator): available_font = abc3d_data.available_fonts[abc3d_data.active_font_index] font_name = available_font.font_name face_name = available_font.face_name - face : Font.FontFace = Font.get_font_face(font_name, face_name) + face: Font.FontFace = Font.get_font_face(font_name, face_name) if face is not None: loaded_glyphs = sorted(face.loaded_glyphs) n = 16 @@ -1451,7 +1465,7 @@ class ABC3D_OT_SaveFontToFile(bpy.types.Operator): row.scale_y = scale_y row.label(text=text) row = layout.row() - export_dir = butils.bpy_to_abspath(abc3d_data.export_dir) + export_dir = butils.bpy_to_abspath(abc3d_data.export_dir) if os.access(export_dir, os.W_OK): self.can_execute = True elif os.path.exists(export_dir): @@ -1463,7 +1477,9 @@ class ABC3D_OT_SaveFontToFile(bpy.types.Operator): row.label(text="Please select another directory") row = layout.row() row.alert = True - elif not utils.can_create_path(export_dir): # does not exist and cannot be created + elif not utils.can_create_path( + export_dir + ): # does not exist and cannot be created self.can_execute = False row.alert = True row.label(text="Directory does not exist and cannot be created") @@ -1472,7 +1488,7 @@ class ABC3D_OT_SaveFontToFile(bpy.types.Operator): row.label(text="Please select another directory") row = layout.row() row.alert = True - elif utils.can_create_path(export_dir): # does not exist and can be created + elif utils.can_create_path(export_dir): # does not exist and can be created self.can_execute = True row.label(text="Directory does not exist") row = layout.row() @@ -1487,7 +1503,9 @@ class ABC3D_OT_SaveFontToFile(bpy.types.Operator): row.prop(abc3d_data, "export_dir") else: - print(f"{utils.prefix()}::save_font_to_file ERROR {face=} {font_name=} {face_name=}") + print( + f"{utils.prefix()}::save_font_to_file ERROR {face=} {font_name=} {face_name=}" + ) print(f"{utils.prefix()} {Font.fonts=}") def execute(self, context): @@ -1500,10 +1518,10 @@ class ABC3D_OT_SaveFontToFile(bpy.types.Operator): "ERROR", [ f"export directory '{abc3d_data.export_dir}' does not exist or is not writable", - "try setting another path" - ] + "try setting another path", + ], ) - return {'CANCELLED'} + return {"CANCELLED"} if not os.path.exists(butils.bpy_to_abspath(abc3d_data.export_dir)): path = butils.bpy_to_abspath(abc3d_data.export_dir) @@ -1515,10 +1533,10 @@ class ABC3D_OT_SaveFontToFile(bpy.types.Operator): "ERROR", [ f"export directory '{abc3d_data.export_dir}' does not exist and cannot be created", - "try setting another path" - ] + "try setting another path", + ], ) - return {'CANCELLED'} + return {"CANCELLED"} fontcollection = bpy.data.collections.get("ABC3D") @@ -1598,9 +1616,11 @@ class ABC3D_OT_SaveFontToFile(bpy.types.Operator): use_selection=True, use_active_scene=True, ) + def delete_scene(): bpy.ops.scene.delete() return None + bpy.app.timers.register(lambda: delete_scene(), first_interval=1) # bpy.ops.scene.delete() @@ -1669,7 +1689,9 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): row.prop(self, "autodetect_names") first_object_name = context.selected_objects[-1].name if self.autodetect_names: - self.font_name, self.face_name = self.do_autodetect_names(first_object_name) + self.font_name, self.face_name = self.do_autodetect_names( + first_object_name + ) if self.autodetect_names: scale_y = 0.5 row = layout.row() @@ -1867,7 +1889,8 @@ def compare_text_object_with_object(t, o, strict=False): # if return True -def link_text_object_with_new_text_properties(text_object, scene = None): + +def link_text_object_with_new_text_properties(text_object, scene=None): lock_depsgraph_updates(auto_unlock_s=-1) butils.link_text_object_with_new_text_properties(text_object, scene) unlock_depsgraph_updates() @@ -1878,12 +1901,12 @@ def detect_text(): scene = bpy.context.scene abc3d_data = scene.abc3d_data required_keys = [ - "type", - "text_id", - "font_name", - "face_name", - "text", - ] + "type", + "text_id", + "font_name", + "face_name", + "text", + ] objects = scene.objects for o in objects: valid = True @@ -1896,10 +1919,7 @@ def detect_text(): if o[butils.get_key("type")] == "textobject": current_text_id = int(o[butils.get_key("text_id")]) text_properties = butils.get_text_properties(current_text_id) - if ( - text_properties is not None - and text_properties.text_object == o - ): + if text_properties is not None and text_properties.text_object == o: # all good pass else: @@ -1972,12 +1992,15 @@ def lock_depsgraph_updates(auto_unlock_s=1): bpy.app.timers.unregister(unlock_depsgraph_updates) bpy.app.timers.register(unlock_depsgraph_updates, first_interval=auto_unlock_s) + def are_depsgraph_updates_locked(): global depsgraph_updates_locked return depsgraph_updates_locked > 0 + import time + @persistent def on_depsgraph_update(scene, depsgraph): if not bpy.context.mode.startswith("EDIT") and not are_depsgraph_updates_locked(): @@ -2010,7 +2033,9 @@ def register(): addon_updater_ops.make_annotations(cls) # Avoid blender 2.8 warnings. bpy.utils.register_class(cls) bpy.types.Scene.abc3d_data = bpy.props.PointerProperty(type=ABC3D_data) - bpy.types.Scene.abc3d_font_creation = bpy.props.PointerProperty(type=ABC3D_PG_FontCreation) + bpy.types.Scene.abc3d_font_creation = bpy.props.PointerProperty( + type=ABC3D_PG_FontCreation + ) # bpy.types.Object.__del__ = lambda self: print(f"Bye {self.name}") # autostart if we load a blend file @@ -2025,6 +2050,7 @@ def register(): if on_depsgraph_update not in bpy.app.handlers.depsgraph_update_post: bpy.app.handlers.depsgraph_update_post.append(on_depsgraph_update) + butils.run_in_main_thread(Font.fonts.clear) butils.run_in_main_thread(butils.clear_available_fonts) butils.run_in_main_thread(butils.register_installed_fonts) butils.run_in_main_thread(butils.update_available_fonts) diff --git a/butils.py b/butils.py index 557054a..2ca1c81 100644 --- a/butils.py +++ b/butils.py @@ -4,6 +4,7 @@ import queue import re import bpy +import bpy_types import mathutils # import time # for debugging performance @@ -45,12 +46,14 @@ def apply_all_transforms(obj): obj.matrix_basis.identity() -def get_parent_collection_names(collection, parent_names): - for parent_collection in bpy.data.collections: - if collection.name in parent_collection.children.keys(): - parent_names.append(parent_collection.name) - get_parent_collection_names(parent_collection, parent_names) - return +# broken +# def get_parent_collection_names(collection, parent_names): +# for parent_collection in bpy.data.collections: +# if collection.name in parent_collection.children.keys(): +# parent_names.append(parent_collection.name) +# get_parent_collection_names(parent_collection, parent_names) +# return + def get_key(key): return f"{utils.prefix()}_{key}" @@ -139,15 +142,15 @@ def calc_tangent_on_bezier(bezier_point_1, bezier_point_2, t): # class TestCalcPoint(): - # co: mathutils.Vector - # handle_left: mathutils.Vector - # handle_right: mathutils.Vector - # def __init__(self, co, handle_left=None, handle_right=None): - # self.co = co - # if handle_left is not None: - # self.handle_left = handle_left - # if handle_right is not None: - # self.handle_right = handle_right +# co: mathutils.Vector +# handle_left: mathutils.Vector +# handle_right: mathutils.Vector +# def __init__(self, co, handle_left=None, handle_right=None): +# self.co = co +# if handle_left is not None: +# self.handle_left = handle_left +# if handle_right is not None: +# self.handle_right = handle_right # a = TestCalcPoint(mathutils.Vector((0,0,0)), handle_right=mathutils.Vector((0,1,0))) @@ -157,7 +160,6 @@ def calc_tangent_on_bezier(bezier_point_1, bezier_point_2, t): # calc_point_on_bezier(a,b,0.5) - def align_rotations_auto_pivot( mask, input_rotations, vectors, factors, local_main_axis ): @@ -251,7 +253,7 @@ def calc_point_on_bezier_spline( # however, maybe let's have it not crash and do this if len(bezier_spline_obj.bezier_points) < 1: print( - "butils::calc_point_on_bezier_spline: whoops, no points. panicking. return 0,0,0" + f"{utils.prefix()}::butils::calc_point_on_bezier_spline: whoops, no points. panicking. return 0,0,0" ) if output_tangent: return mathutils.Vector((0, 0, 0)), mathutils.Vector((1, 0, 0)) @@ -275,8 +277,9 @@ def calc_point_on_bezier_spline( # if the bezier points sit on each other we have same issue # but that is then to be fixed in the bezier if p.handle_left == p.co and len(bezier_spline_obj.bezier_points) > 1: - beziers, lengths, total_length = get_real_beziers_and_lengths(bezier_spline_obj, - resolution_factor) + beziers, lengths, total_length = get_real_beziers_and_lengths( + bezier_spline_obj, resolution_factor + ) travel_point = calc_point_on_bezier(beziers[0][1], beziers[0][0], 0.001) travel = travel_point.normalized() * distance @@ -288,8 +291,9 @@ def calc_point_on_bezier_spline( else: return location - beziers, lengths, total_length = get_real_beziers_and_lengths(bezier_spline_obj, - resolution_factor) + beziers, lengths, total_length = get_real_beziers_and_lengths( + bezier_spline_obj, resolution_factor + ) iterated_distance = 0 for i in range(0, len(beziers)): @@ -307,7 +311,7 @@ def calc_point_on_bezier_spline( # if we are here, the point is outside the spline last_i = len(beziers) - 1 p = beziers[last_i][1] - + travel = (p.handle_right - p.co).normalized() * (distance - total_length) # in case the handles sit on the points @@ -368,6 +372,7 @@ def calc_point_on_bezier_curve( # and should not happen usually return bezier_curve_obj.matrix_world @ mathutils.Vector((distance, 0, 0)) + # def get_objects_by_name(name, startswith="", endswith=""): # return [obj for obj in bpy.context.scene.objects if obj.name.startswith(startswith) and if obj.name.endswith(endswith)] @@ -397,13 +402,14 @@ def find_objects_by_custom_property(objects, property_name="", property_value="" ] -def turn_collection_hierarchy_into_path(obj): - parent_collection = obj.users_collection[0] - parent_names = [] - parent_names.append(parent_collection.name) - get_parent_collection_names(parent_collection, parent_names) - parent_names.reverse() - return "\\".join(parent_names) +# not verified +# def turn_collection_hierarchy_into_path(obj): +# parent_collection = obj.users_collection[0] +# parent_names = [] +# parent_names.append(parent_collection.name) +# get_parent_collection_names(parent_collection, parent_names) +# parent_names.reverse() +# return "\\".join(parent_names) def find_font_object(fontcollection, font_name): @@ -458,7 +464,9 @@ def move_in_fontcollection(obj, fontcollection, allow_duplicates=False): fontcollection.objects.link(glyphs_obj) glyphs_obj.parent = face_obj elif len(glyphs_objs) > 1: - print("found more glyphs objects than expected") + print( + f"{utils.prefix()}::move_in_fontcollection: found more glyphs objects than expected" + ) # now it must exist glyphs_obj = find_objects_by_name(face_obj.children, startswith="glyphs")[0] @@ -563,10 +571,14 @@ def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""): for mff in modified_font_faces: mff_glyphs = [] - face : Font.FontFace = Font.get_font_face(mff["font_name"], mff["face_name"]) + face: Font.FontFace = Font.get_font_face(mff["font_name"], mff["face_name"]) if face is None: - print(f"{utils.prefix()}::load_font_from_path({filepath=}, {glyphs=}, {font_name=}, {face_name=}) failed") - print(f"modified font face {mff=} could not be accessed.") + print( + f"{utils.prefix()}::load_font_from_path({filepath=}, {glyphs=}, {font_name=}, {face_name=}) failed" + ) + print( + f"{utils.prefix()}:: modified font face {mff=} could not be accessed." + ) continue # iterate glyphs for g in face.glyphs: @@ -592,6 +604,143 @@ def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""): # completely_delete_objects(remove_list) +def is_glyph_used(glyph_alternates): + fontcollection: bpy_types.Collection = bpy.data.collections.get("ABC3D") + glyph = bpy.types.PointerProperty + for glyph in glyph_alternates: + for o in bpy.context.scene.objects: + # only check other glyphs + if is_glyph_object(o): + # then attempt to compare properties + if ( + get_key("font_name") in o + and get_key("face_name") in o + and get_key("glyph_id") in o + and o[get_key("font_name")] == glyph["font_name"] + and o[get_key("face_name")] == glyph["face_name"] + and o[get_key("glyph_id")] == glyph["glyph"] + ): + # following check is not necessary, + # but we leave it in for backwards compatibility + # properties in the fontcollection start with prefix + # and so they should be caught by previous check + if fontcollection.users == 0 or not ( + fontcollection in o.users_collection + and len(o.users_collection) <= 1 + ): + # it's in the scene and has the correct properties + # it is used + return True + # following check is possibly overkill + # but we also check for objects that use the data + # and are not glyph objects, in that case we don't pull the data + # from under their feet + if is_mesh(o) and o.data == glyph.data: + # in this case, yes we need to check if it is a glyph in the fontcollection + if fontcollection.users == 0 or not ( + fontcollection in o.users_collection + and len(o.users_collection) <= 1 + ): + # bam! + return True + # whoosh! + return False + + +def clean_fontcollection(fontcollection=None): + if fontcollection is None: + fontcollection = bpy.data.collections.get("ABC3D") + if fontcollection is None: + print( + f"{utils.prefix()}::clean_fontcollection: failed beacause fontcollection is none" + ) + return False + + collection_fonts = find_objects_by_custom_property( + fontcollection.all_objects, "is_font", True + ) + + delete_these_fonts = [] + delete_these_font_faces = [] + delete_these_glyph_moms = [] + for font_and_face in Font.get_loaded_fonts_and_faces(): + font_name = font_and_face[0] + face_name = font_and_face[1] + + collection_font_list = find_objects_by_custom_property( + collection_fonts, "font_name", font_name + ) + for collection_font in collection_font_list: + collection_font_face_list = find_objects_by_custom_property( + collection_font.children, "face_name", face_name + ) + count_font_faces = 0 + for collection_font_face in collection_font_face_list: + glyphs_mom_list = find_objects_by_name( + collection_font_face.children, startswith="glyphs" + ) + count_glyphs_moms = 0 + for glyphs_mom in glyphs_mom_list: + if len(glyphs_mom.children) == 0: + delete_these_glyph_moms.append(glyphs_mom) + count_glyphs_moms += 1 + if len(collection_font_face.children) == count_glyphs_moms: + delete_these_font_faces.append(collection_font_face) + count_font_faces += 1 + if len(collection_font.children) == count_font_faces: + delete_these_fonts.append(collection_font) + + completely_delete_objects(delete_these_glyph_moms) + completely_delete_objects(delete_these_font_faces) + completely_delete_objects(delete_these_fonts) + + +def unload_unused_glyph(font_name, face_name, glyph_id, do_clean_fontcollection=True): + fontcollection: bpy_types.Collection = bpy.data.collections.get("ABC3D") + glyph_variations = Font.get_glyphs(font_name, face_name, glyph_id) + if is_glyph_used(glyph_variations): + return False + + delete_these = [] + for glyph_pointer in glyph_variations: + for o in fontcollection.all_objects: + if ( + is_glyph_object(o) + and o["font_name"] == font_name + and o["face_name"] == face_name + and o["glyph"] == glyph_id + ): + if len(o.users_collection) <= 1: + delete_these.append(o) + completely_delete_objects(delete_these) + + Font.unloaded_glyph(font_name, face_name, glyph_id) + if do_clean_fontcollection: + clean_fontcollection(fontcollection) + return True + + +def unload_unused_glyphs(do_clean_fontcollection=True): + fontcollection: bpy_types.Collection = bpy.data.collections.get("ABC3D") + if fontcollection is not None: + for font_and_face in Font.get_loaded_fonts_and_faces(): + font_name = font_and_face[0] + face_name = font_and_face[1] + face: Font.FontFace | None = Font.get_font_face(font_name, face_name) + if face is None: + print( + f"{utils.prefix()}::unload_unused_glyphs: face is None {font_name=} {face_name=}" + ) + continue + unloaded_these = [] + for glyph_id in face.loaded_glyphs.copy(): + unload_unused_glyph( + font_name, face_name, glyph_id, do_clean_fontcollection=False + ) + if do_clean_fontcollection: + clean_fontcollection(fontcollection) + + def update_available_fonts(): abc3d_data = bpy.context.scene.abc3d_data @@ -606,7 +755,9 @@ def update_available_fonts(): f = abc3d_data.available_fonts.add() f.font_name = font_name f.face_name = face_name - print(f"{utils.prefix()}::update_available_fonts: {__name__} added {font_name} {face_name}") + print( + f"{utils.prefix()}::update_available_fonts: {__name__} added {font_name} {face_name}" + ) # def update_available_texts(): @@ -724,6 +875,8 @@ def completely_delete_objects(objs, recursive=True): except ReferenceError: # not important pass + except RuntimeError: + pass def is_mesh(o): @@ -753,7 +906,7 @@ def is_glyph_object(o): return o[f"{utils.prefix()}_type"] == "glyph" try: return ( - type(o.parent) is not type(None) + o.parent is not None and "glyphs" in o.parent.name and is_mesh(o) and not is_metrics_object(o) @@ -792,6 +945,7 @@ 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 get_glyph_prepost_advances(glyph_obj): for c in glyph_obj.children: if is_metrics_object(c): @@ -807,14 +961,16 @@ def get_glyph_height(glyph_obj): def prepare_text(font_name, face_name, text, allow_replacement=True): - availability = Font.test_glyphs_availability( - font_name, face_name, text - ) + availability = Font.test_glyphs_availability(font_name, face_name, text) if isinstance(availability, int): if availability == Font.MISSING_FONT: - print(f"{utils.prefix()}::prepare_text({font_name=}, {face_name=}, {text=}) failed with MISSING_FONT") + print( + f"{utils.prefix()}::prepare_text({font_name=}, {face_name=}, {text=}) failed with MISSING_FONT" + ) if availability is Font.MISSING_FACE: - print(f"{utils.prefix()}::prepare_text({font_name=}, {face_name=}, {text=}) failed with MISSING_FACE") + print( + f"{utils.prefix()}::prepare_text({font_name=}, {face_name=}, {text=}) failed with MISSING_FACE" + ) return False loadable = availability.unloaded # possibly replace upper and lower case letters with each other @@ -832,9 +988,16 @@ def prepare_text(font_name, face_name, text, allow_replacement=True): load_font_from_filepath(filepath, loadable, font_name, face_name) return True + def predict_actual_text(text_properties): - availability = Font.test_availability(text_properties.font_name, text_properties.face_name, text_properties.text) - AVAILABILITY = Font.test_availability(text_properties.font_name, text_properties.face_name, text_properties.text.swapcase()) + availability = Font.test_availability( + text_properties.font_name, text_properties.face_name, text_properties.text + ) + AVAILABILITY = Font.test_availability( + text_properties.font_name, + text_properties.face_name, + text_properties.text.swapcase(), + ) t_text = text_properties.text for c in availability.missing: t_text = t_text.replace(c, "") @@ -842,6 +1005,7 @@ def predict_actual_text(text_properties): t_text = t_text.replace(c, "") return t_text + def is_bezier(curve): if curve.type != "CURVE": return False @@ -906,6 +1070,7 @@ COMPARE_TEXT_OBJECT_SAME = 0 COMPARE_TEXT_OBJECT_DIFFER = 1 COMPARE_TEXT_OBJECT_REGENERATE = 2 + def find_free_text_id(): scene = bpy.context.scene abc3d_data = scene.abc3d_data @@ -922,21 +1087,27 @@ def find_free_text_id(): found_free = True return text_id + def compare_text_properties_to_text_object(text_properties, o): for key in text_object_keys: if key in ignore_keys_in_text_object_comparison: continue object_key = get_key(key) - text_property = text_properties[key] if key in text_properties else getattr(text_properties, key) + text_property = ( + text_properties[key] + if key in text_properties + else getattr(text_properties, key) + ) text_object_property = o[object_key] if object_key in o else False if text_property != text_object_property: if key in keys_trigger_regeneration: return COMPARE_TEXT_OBJECT_REGENERATE elif key in ["translation", "orientation"]: if ( - text_property[0] != text_object_property[0] or - text_property[1] != text_object_property[1] or - text_property[2] != text_object_property[2]): + text_property[0] != text_object_property[0] + or text_property[1] != text_object_property[1] + or text_property[2] != text_object_property[2] + ): return COMPARE_TEXT_OBJECT_DIFFER # else same else: @@ -950,16 +1121,17 @@ def transfer_text_properties_to_text_object(text_properties, o): if key in ignore_keys_in_text_object_comparison: continue object_key = get_key(key) - text_property = text_properties[key] if key in text_properties else getattr(text_properties, key) + text_property = ( + text_properties[key] + if key in text_properties + else getattr(text_properties, key) + ) o[object_key] = text_property o[get_key("type")] = "textobject" def get_glyph(glyph_id, font_name, face_name, notify_on_replacement=False): - glyph_tmp = Font.get_glyph(font_name, - face_name, - glyph_id, - -1) + glyph_tmp = Font.get_glyph(font_name, face_name, glyph_id, -1) if glyph_tmp is None: space_width = Font.is_space(glyph_id) if space_width: @@ -973,7 +1145,7 @@ def get_glyph(glyph_id, font_name, face_name, notify_on_replacement=False): text_properties.font_name, text_properties.face_name, possible_replacement, - -1 + -1, ) if glyph_tmp is not None: message = message + f" (replaced with '{possible_replacement}')" @@ -991,7 +1163,8 @@ def get_glyph(glyph_id, font_name, face_name, notify_on_replacement=False): return glyph_tmp.original -def get_text_properties(text_id, scene = None): + +def get_text_properties(text_id, scene=None): if scene is None: scene = bpy.context.scene abc3d_data = scene.abc3d_data @@ -1000,7 +1173,15 @@ def get_text_properties(text_id, scene = None): return t return None -def duplicate(obj, data=True, actions=True, add_to_collection=True, collection=None, recursive=True): + +def duplicate( + obj, + data=True, + actions=True, + add_to_collection=True, + collection=None, + recursive=True, +): obj_copy = obj.copy() if add_to_collection: if collection: @@ -1019,8 +1200,13 @@ def duplicate(obj, data=True, actions=True, add_to_collection=True, collection=N # child_copy.matrix_parent_inverse = obj_copy.matrix_world.inverted() return obj_copy -def transfer_text_object_to_text_properties(text_object, text_properties, id_from_text_properties=True): - possible_brother_text_id = text_object[get_key("text_id")] if get_key("text_id") in text_object else "" + +def transfer_text_object_to_text_properties( + text_object, text_properties, id_from_text_properties=True +): + possible_brother_text_id = ( + text_object[get_key("text_id")] if get_key("text_id") in text_object else "" + ) for key in text_object_keys: if key in ignore_keys_in_text_object_comparison: continue @@ -1028,12 +1214,17 @@ def transfer_text_object_to_text_properties(text_object, text_properties, id_fro if id_from_text_properties and key == "text_id": text_object[object_key] = text_properties["text_id"] else: - text_object_property = text_object[object_key] if object_key in text_object else False + text_object_property = ( + text_object[object_key] if object_key in text_object else False + ) if text_object_property is not False: text_properties[key] = text_object_property if len(text_object.children) == 0: - if possible_brother_text_id != text_properties["text_id"] and possible_brother_text_id != "": + if ( + possible_brother_text_id != text_properties["text_id"] + and possible_brother_text_id != "" + ): possible_brother_properties = get_text_properties(possible_brother_text_id) possible_brother_object = possible_brother_properties.text_object if possible_brother_object is not None: @@ -1047,11 +1238,7 @@ def transfer_text_object_to_text_properties(text_object, text_properties, id_fro found_reconstructable_glyphs = False glyph_objects_with_indices = [] - required_keys = [ - "glyph_index", - "glyph_id", - "type" - ] + required_keys = ["glyph_index", "glyph_id", "type"] for glyph_object in text_object.children: if is_glyph_object(glyph_object): has_required_keys = True @@ -1087,8 +1274,8 @@ def transfer_text_object_to_text_properties(text_object, text_properties, id_fro for glyph_index, glyph_object in enumerate(glyph_objects_with_indices): glyph_id = glyph_object[get_key("glyph_id")] # glyph_tmp = Font.get_glyph(text_properties.font_name, - # text_properties.face_name, - # glyph_id) + # text_properties.face_name, + # glyph_id) # glyph = glyph_tmp.original glyph_properties = text_properties.glyphs.add() @@ -1111,8 +1298,10 @@ def transfer_text_object_to_text_properties(text_object, text_properties, id_fro text_properties.glyphs.clear() unfortunate_children = text_object.children completely_delete_objects(unfortunate_children) + def kill_children(): completely_delete_objects(unfortunate_children) + run_in_main_thread(kill_children) if "font_name" in text_properties and "face_name" in text_properties: @@ -1128,9 +1317,11 @@ def link_text_object_with_new_text_properties(text_object, scene=None): text_properties = scene.abc3d_data.available_texts.add() text_properties["text_id"] = text_id # text_object[get_key("text_id")] = text_id - prepare_text(text_object[get_key("font_name")], - text_object[get_key("face_name")], - text_object[get_key("text")]) + prepare_text( + text_object[get_key("font_name")], + text_object[get_key("face_name")], + text_object[get_key("text")], + ) text_properties.text_object = text_object transfer_text_object_to_text_properties(text_object, text_properties) @@ -1144,14 +1335,14 @@ def test_finding(): o = bpy.context.active_object transfer_text_object_to_text_properties(o, t) -# def detect_texts(): - # scene = bpy.context.scene - # abc3d_data = scene.abc3d_data - # for o in bpy.data.objects: - # if get_key("type") in o \ - # and o[get_key("type") == "textobject" \ - # and o[get_key("t +# def detect_texts(): +# scene = bpy.context.scene +# abc3d_data = scene.abc3d_data +# for o in bpy.data.objects: +# if get_key("type") in o \ +# and o[get_key("type") == "textobject" \ +# and o[get_key("t def link_text_object_and_text_properties(o, text_properties): @@ -1159,22 +1350,33 @@ def link_text_object_and_text_properties(o, text_properties): o["text_id"] = text_id text_properties.textobject = o + def get_glyph_object_property(text_properties, glyph_properties, key): if key in glyph_properties: return glyph_properties[key] if hasattr(glyph_properties, key): return getattr(glyph_properties, key) - return text_properties[key] if key in text_properties else getattr(text_properties, key) + return ( + text_properties[key] + if key in text_properties + else getattr(text_properties, key) + ) -def transfer_properties_to_glyph_object(text_properties, glyph_properties, glyph_object): + +def transfer_properties_to_glyph_object( + text_properties, glyph_properties, glyph_object +): for key in glyph_object_keys: if key in ignore_keys_in_glyph_object_transfer: continue object_key = get_key(key) - glyph_object[object_key] = get_glyph_object_property(text_properties, glyph_properties, key) + glyph_object[object_key] = get_glyph_object_property( + text_properties, glyph_properties, key + ) glyph_object[get_key("type")] = "glyph" glyph_object[get_key("text_id")] = text_properties["text_id"] + def transfer_glyph_object_to_glyph_properties(glyph_object, glyph_properties): for key in glyph_object_keys: if key in ignore_keys_in_glyph_object_transfer: @@ -1182,6 +1384,7 @@ def transfer_glyph_object_to_glyph_properties(glyph_object, glyph_properties): glyph_properties[key] = glyph_object[get_key(key)] glyph_properties["text_id"] = glyph_object[get_key("text_id")] + def would_regenerate(text_properties): predicted_text = predict_actual_text(text_properties) if text_properties.actual_text != predicted_text: @@ -1217,10 +1420,11 @@ def update_matrices(obj): if obj.parent is None: obj.matrix_world = obj.matrix_basis - # else: - obj.matrix_world = obj.parent.matrix_world * \ - obj.matrix_parent_inverse * \ - obj.matrix_basis + # else: + obj.matrix_world = ( + obj.parent.matrix_world * obj.matrix_parent_inverse * obj.matrix_basis + ) + def is_or_has_parent(o, parent, if_is_parent=True, max_depth=10): if o == parent and if_is_parent: @@ -1234,23 +1438,26 @@ def is_or_has_parent(o, parent, if_is_parent=True, max_depth=10): return False return False + def parent_to_curve(o, c): - o.parent_type = 'OBJECT' + o.parent_type = "OBJECT" o.parent = c # o.matrix_parent_inverse = c.matrix_world.inverted() - + if c.data.use_path and len(c.data.splines) > 0: if c.data.splines[0].type == "BEZIER": i = -1 if c.data.splines[0].use_cyclic_u else 0 p = c.data.splines[0].bezier_points[i].co o.matrix_parent_inverse.translation = p * -1.0 - elif c.data.splines[0].type == 'NURBS': + elif c.data.splines[0].type == "NURBS": cm = c.to_mesh() p = cm.vertices[0].co o.matrix_parent_inverse.translation = p * -1.0 -def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, can_regenerate=False): +def set_text_on_curve( + text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, can_regenerate=False +): """set_text_on_curve An earlier reset cancels the other. @@ -1273,7 +1480,6 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, return False distribution_type = "CALCULATE" if is_bezier(mom) else "FOLLOW_PATH" - # NOTE: following not necessary anymore # as we fixed data_path with parent_to_curve trick @@ -1283,9 +1489,9 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, # https://projects.blender.org/blender/blender/issues/100661 # previous_use_path = mom.data.use_path # if distribution_type == "CALCULATE": - # mom.data.use_path = False + # mom.data.use_path = False # elif distribution_type == "FOLLOW_PATH": - # mom.data.use_path = True + # mom.data.use_path = True regenerate = can_regenerate and would_regenerate(text_properties) @@ -1330,10 +1536,9 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, ############### GET GLYPH - glyph_tmp = Font.get_glyph(text_properties.font_name, - text_properties.face_name, - glyph_id, - -1) + glyph_tmp = Font.get_glyph( + text_properties.font_name, text_properties.face_name, glyph_id, -1 + ) if glyph_tmp is None: space_width = Font.is_space(glyph_id) if space_width: @@ -1348,7 +1553,7 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, text_properties.font_name, text_properties.face_name, possible_replacement, - -1 + -1, ) if glyph_tmp is not None: message = message + f" (replaced with '{possible_replacement}')" @@ -1368,7 +1573,11 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, ############### GLYPH PROPERTIES - glyph_properties = text_properties.glyphs[glyph_index] if not regenerate else text_properties.glyphs.add() + glyph_properties = ( + text_properties.glyphs[glyph_index] + if not regenerate + else text_properties.glyphs.add() + ) if regenerate: glyph_properties["glyph_id"] = glyph_id @@ -1383,14 +1592,16 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, if regenerate: outer_node = bpy.data.objects.new(f"{glyph_id}", None) inner_node = bpy.data.objects.new(f"{glyph_id}_mesh", glyph.data) - transfer_properties_to_glyph_object(text_properties, glyph_properties, outer_node) + transfer_properties_to_glyph_object( + text_properties, glyph_properties, outer_node + ) # Add into the scene. mom.users_collection[0].objects.link(outer_node) mom.users_collection[0].objects.link(inner_node) # Parenting is hard. - inner_node.parent_type = 'OBJECT' + inner_node.parent_type = "OBJECT" inner_node.parent = outer_node inner_node.matrix_parent_inverse = outer_node.matrix_world.inverted() parent_to_curve(outer_node, mom) @@ -1426,7 +1637,9 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, outer_node.constraints.new(type="FOLLOW_PATH") outer_node.constraints["Follow Path"].target = mom outer_node.constraints["Follow Path"].use_fixed_location = True - outer_node.constraints["Follow Path"].offset_factor = applied_advance / curve_length + outer_node.constraints["Follow Path"].offset_factor = ( + applied_advance / curve_length + ) outer_node.constraints["Follow Path"].use_curve_follow = True outer_node.constraints["Follow Path"].forward_axis = "FORWARD_X" outer_node.constraints["Follow Path"].up_axis = "UP_Y" @@ -1442,7 +1655,9 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, previous_inner_node_rotation_mode = inner_node.rotation_mode # get info from bezier - location, tangent, spline_index = calc_point_on_bezier_curve(mom, applied_advance, True, True) + location, tangent, spline_index = calc_point_on_bezier_curve( + mom, applied_advance, True, True + ) # check if we are on a new line if spline_index != previous_spline_index: @@ -1457,13 +1672,19 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, vectors = [tangent] factors = [1.0] local_main_axis = mathutils.Vector((1.0, 0.0, 0.0)) - motor = align_rotations_auto_pivot( - mask, input_rotations, vectors, factors, local_main_axis - ) if not text_properties.ignore_orientation else [mathutils.Matrix()] + motor = ( + align_rotations_auto_pivot( + mask, input_rotations, vectors, factors, local_main_axis + ) + if not text_properties.ignore_orientation + else [mathutils.Matrix()] + ) q = mathutils.Quaternion() q.rotate(text_properties.orientation) - outer_node.rotation_quaternion = (motor[0].to_3x3() @ q.to_matrix()).to_quaternion() + outer_node.rotation_quaternion = ( + motor[0].to_3x3() @ q.to_matrix() + ).to_quaternion() # # NOTE: supercool but out of scope, as we wouldhave to update it everytime the curve object rotates, # # but this would ignore the curve objects orientation: @@ -1482,16 +1703,16 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, ############### PREPARE FOR THE NEXT glyph_advance = ( - glyph_post_advance * scalor + text_properties.letter_spacing + glyph_properties.letter_spacing + glyph_post_advance * scalor + + text_properties.letter_spacing + + glyph_properties.letter_spacing ) # now we need to compensate for curvature # otherwise letters will be closer together the curvier the bezier is # NOTE: this could be done more efficiently curve_compensation = 0 - if distribution_type == "CALCULATE" and ( - not is_newline or spline_index == 0 - ): + if distribution_type == "CALCULATE" and (not is_newline or spline_index == 0): if text_properties.compensate_curvature and glyph_advance > 0: previous_location, psi = calc_point_on_bezier_curve( mom, advance, False, True @@ -1503,8 +1724,10 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, n_max = 100 n = 0 while ( - previous_location - new_location - ).length > glyph_advance and psi == si and n < n_max: + (previous_location - new_location).length > glyph_advance + and psi == si + and n < n_max + ): curve_compensation = curve_compensation - glyph_advance * 0.01 tmp_new_location, si = calc_point_on_bezier_curve( mom, @@ -1513,14 +1736,18 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, output_spline_index=True, ) if tmp_new_location == new_location: - print(f"{utils.prefix()}::set_text_on_curve::compensate_curvature while loop overstaying welcome") + print( + f"{utils.prefix()}::set_text_on_curve::compensate_curvature while loop overstaying welcome" + ) break new_location = tmp_new_location n += 1 n = 0 while ( - previous_location - new_location - ).length < glyph_advance and psi == si and n < n_max: + (previous_location - new_location).length < glyph_advance + and psi == si + and n < n_max + ): curve_compensation = curve_compensation + glyph_advance * 0.01 tmp_new_location, si = calc_point_on_bezier_curve( mom, @@ -1529,7 +1756,9 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, output_spline_index=True, ) if tmp_new_location == new_location: - print(f"{utils.prefix()}::set_text_on_curve::compensate_curvature while loop overstaying welcome") + print( + f"{utils.prefix()}::set_text_on_curve::compensate_curvature while loop overstaying welcome" + ) break new_location = tmp_new_location n += 1 @@ -1813,6 +2042,8 @@ def add_default_metrics_to_objects(objects=None, overwrite_existing=False): targets = [] reference_bound_box = None for o in objects: + if not hasattr(o, "parent"): + print(f"{o.name} has not a PARENTNTNTNTNTNNTNTNTNTNTN") is_possibly_glyph = is_glyph(o) if is_possibly_glyph: metrics = [] @@ -1932,6 +2163,7 @@ def align_metrics_of_objects(objects=None): add_metrics_obj_from_bound_box(t, bound_box) return "" + def align_origins_to_active_object(objects=None, axis=2): if objects is None: objects = bpy.context.selected_objects @@ -1954,87 +2186,87 @@ def align_origins_to_active_object(objects=None, axis=2): v.co[axis] -= diff o.matrix_world.translation[axis] = reference_origin_position - + return "" + # NOTE: # Following code is not necessary anymore, # as we derive the advance through metrics # boundaries # def divide_vectors(v1=mathutils.Vector((1.0,1.0,1.0)), v2=mathutils.Vector((1.0,1.0,1.0))): - # return mathutils.Vector([v1[i] / v2[i] for i in range(3)]) +# return mathutils.Vector([v1[i] / v2[i] for i in range(3)]) # def get_origin_shift_metrics(o, axis=0): - # if not is_metrics_object(o): - # return False - # min_value = sys.float_info.max - # for v in o.data.vertices: - # if v.co[axis] < min_value: - # min_value = v.co[axis] - # if min_value == sys.float_info.max: - # return False - # return min_value +# if not is_metrics_object(o): +# return False +# min_value = sys.float_info.max +# for v in o.data.vertices: +# if v.co[axis] < min_value: +# min_value = v.co[axis] +# if min_value == sys.float_info.max: +# return False +# return min_value # def fix_origin_shift_metrics(o, axis=0): - # shift = get_origin_shift_metrics(o) - # if not shift: - # print("False") - # return False - # for v in o.data.vertices: - # v.co[axis] -= shift - # shift_vector = mathutils.Vector((0.0, 0.0, 0.0)) - # shift_vector[axis] = shift - # # o.location = o.location - (divide_vectors(v2=o.matrix_world.to_scale()) * (o.matrix_world @ shift_vector)) - # o.matrix_local.translation = o.matrix_local.translation + (shift_vector @ o.matrix_local.inverted()) - # # update_matrices(o) - # return True +# shift = get_origin_shift_metrics(o) +# if not shift: +# print("False") +# return False +# for v in o.data.vertices: +# v.co[axis] -= shift +# shift_vector = mathutils.Vector((0.0, 0.0, 0.0)) +# shift_vector[axis] = shift +# # o.location = o.location - (divide_vectors(v2=o.matrix_world.to_scale()) * (o.matrix_world @ shift_vector)) +# o.matrix_local.translation = o.matrix_local.translation + (shift_vector @ o.matrix_local.inverted()) +# # update_matrices(o) +# return True # def fix_objects_metrics_origins(objects=None, axis=0, handle_metrics_directly=True): - # if objects is None: - # objects = bpy.context.selected_objects - # if len(objects) == 0: - # return "no objects selected" +# if objects is None: +# objects = bpy.context.selected_objects +# if len(objects) == 0: +# return "no objects selected" - # for o in objects: - # is_possibly_glyph = is_glyph(o) - # if is_possibly_glyph: - # for c in o.children: - # if is_metrics_object(c): - # fix_origin_shift_metrics(c, axis) - # elif is_metrics_object(o) and handle_metrics_directly: - # fix_origin_shift_metrics(o, axis) - # return "" +# for o in objects: +# is_possibly_glyph = is_glyph(o) +# if is_possibly_glyph: +# for c in o.children: +# if is_metrics_object(c): +# fix_origin_shift_metrics(c, axis) +# elif is_metrics_object(o) and handle_metrics_directly: +# fix_origin_shift_metrics(o, axis) +# return "" # def align_origins_to_metrics(objects=None): - # if objects is None: - # objects = bpy.context.selected_objects - # if len(objects) == 0: - # return "no objects selected" +# if objects is None: +# objects = bpy.context.selected_objects +# if len(objects) == 0: +# return "no objects selected" - # for o in objects: - # is_possibly_glyph = is_glyph(o) - # if is_possibly_glyph: - # min_x = 9999999999 - # for c in o.children: - # if is_metrics_object(c): - # for v in c.data.vertices: - # if v.co[0] < min_x: - # min_x = v.co[0] +# for o in objects: +# is_possibly_glyph = is_glyph(o) +# if is_possibly_glyph: +# min_x = 9999999999 +# for c in o.children: +# if is_metrics_object(c): +# for v in c.data.vertices: +# if v.co[0] < min_x: +# min_x = v.co[0] - # metrics_origin_x = c.matrix_world.translation[0] + min_x - - # diff = metrics_origin_x - o.matrix_world.translation[0] +# metrics_origin_x = c.matrix_world.translation[0] + min_x - # for v in o.data.vertices: - # v.co[0] -= diff +# diff = metrics_origin_x - o.matrix_world.translation[0] - # o.location += mathutils.Vector((diff, 0.0, 0.0)) @ o.matrix_world.inverted() +# for v in o.data.vertices: +# v.co[0] -= diff - # for c in o.children: - # if is_metrics_object(c): - # c.location -= mathutils.Vector((diff, 0.0, 0.0)) @ o.matrix_world.inverted() +# o.location += mathutils.Vector((diff, 0.0, 0.0)) @ o.matrix_world.inverted() - # return "" +# for c in o.children: +# if is_metrics_object(c): +# c.location -= mathutils.Vector((diff, 0.0, 0.0)) @ o.matrix_world.inverted() +# return "" diff --git a/common/Font.py b/common/Font.py index 4d0c6a7..d68518c 100644 --- a/common/Font.py +++ b/common/Font.py @@ -1,6 +1,5 @@ -from typing import Dict from pathlib import Path -from typing import NamedTuple +from typing import Dict, NamedTuple # convenience dictionary for translating names to glyph ids # note: overwritten/extended by the content of "glypNamesToUnicode.txt" @@ -161,7 +160,6 @@ class Font: self.faces = faces - def register_font(font_name, face_name, glyphs_in_fontfile, filepath): if not fonts.keys().__contains__(font_name): fonts[font_name] = Font({}) @@ -250,7 +248,10 @@ def get_glyphs(font_name, face_name, glyph_id): face = get_font_face(font_name, face_name) if face is None: print(f"ABC3D::get_glyph: font({font_name}) face({face_name}) not found") - print(fonts[font_name].faces.keys()) + try: + print(fonts[font_name].faces.keys()) + except: + print(fonts.keys()) return [] glyphs_for_id = face.glyphs.get(glyph_id) @@ -291,6 +292,19 @@ def get_glyph(font_name, face_name, glyph_id, alternate=0): return glyphs[alternate] +def unloaded_glyph(font_name, face_name, glyph_id): + face = get_font_face(font_name, face_name) + if face is None: + print(f"ABC3D::get_glyph: font({font_name}) face({face_name}) not found") + return + while True: + try: + fonts[font_name].faces[face_name].loaded_glyphs.remove(glyph_id) + del fonts[font_name].faces[face_name].glyphs[glyph_id] + except ValueError: + break + + class GlyphsAvailability(NamedTuple): loaded: str missing: str