From 335ab1facea387ca6fab652a6bff22ec8df620a5 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Thu, 29 May 2025 15:27:24 +0200 Subject: [PATCH] stabilizing, user experience use class for glyph availability use isinstance instead of type better user experience when export directory does not exist --- __init__.py | 218 ++++++++++++++++++++++++++++++++---------------- butils.py | 62 +++++++------- common/Font.py | 108 ++++++++++++++++++------ common/utils.py | 20 +++++ 4 files changed, 279 insertions(+), 129 deletions(-) diff --git a/__init__.py b/__init__.py index 3d7f6bc..69331d8 100644 --- a/__init__.py +++ b/__init__.py @@ -35,7 +35,6 @@ if "Font" in locals(): importlib.reload(bimport) importlib.reload(addon_updater_ops) - def getPreferences(context): preferences = context.preferences return preferences.addons[__name__].preferences @@ -398,51 +397,51 @@ 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 - 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="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 - ] + face : Font.FontFace = Font.get_font_face(font_name, face_name) + if face is not None: + available_glyphs = face.glyphs_in_fontfile + loaded_glyphs = sorted(face.loaded_glyphs) + box = layout.box() + box.row().label(text=f"Font Name: {font_name}") + box.row().label(text=f"Face Name: {face_name}") + n = 16 + n_rows = int(len(available_glyphs) / n) + box.row().label(text="Glyphs:") + subbox = box.box() + for i in range(0, n_rows + 1): + text = "".join( + [ + f"{u}" + for ui, u in enumerate(available_glyphs) + if ui < (i + 1) * n and ui >= i * n + ] + ) + scale_y = 0.5 + row = subbox.row() + row.scale_y = scale_y + row.alignment = "CENTER" + row.label(text=text) + n_rows = int(len(loaded_glyphs) / n) + box.row().label(text="Loaded/Used Glyphs:") + subbox = box.box() + for i in range(0, n_rows + 1): + text = "".join( + [ + f"{u}" + for ui, u in enumerate(loaded_glyphs) + if ui < (i + 1) * n and ui >= i * n + ] + ) + scale_y = 0.5 + row = subbox.row() + row.scale_y = scale_y + row.label(text=text) + row = layout.row() + oper_lf = row.operator( + f"{__name__}.load_font", text="Load all glyphs in memory" ) - scale_y = 0.5 - row = subbox.row() - row.scale_y = scale_y - row.alignment = "CENTER" - row.label(text=text) - n_rows = int(len(loaded_glyphs) / n) - box.row().label(text="Loaded/Used Glyphs:") - subbox = box.box() - for i in range(0, n_rows + 1): - text = "".join( - [ - f"{u}" - for ui, u in enumerate(loaded_glyphs) - if ui < (i + 1) * n and ui >= i * n - ] - ) - scale_y = 0.5 - row = subbox.row() - row.scale_y = scale_y - row.label(text=text) - row = layout.row() - oper_lf = row.operator( - f"{__name__}.load_font", text="Load all glyphs in memory" - ) - oper_lf.font_name = font_name - oper_lf.face_name = face_name + oper_lf.font_name = font_name + oper_lf.face_name = face_name class ABC3D_PT_TextPlacement(bpy.types.Panel): @@ -754,7 +753,6 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): return t return None - # NOTE: HERE def get_active_glyph_properties(self): a_o = bpy.context.active_object if a_o is not None: @@ -1026,7 +1024,11 @@ class ABC3D_OT_LoadFont(bpy.types.Operator): face_name: bpy.props.StringProperty() def execute(self, context): - filepaths = Font.fonts[self.font_name].faces[self.face_name].filepaths + face : Font.FontFace = Font.get_font_face(self.font_name, self.face_name) + if face is None: + butils.ShowMessageBox(f"{utils.prefix()} Load Font", icon="ERROR", message=["Could not load font, sorry!", f"{self.font_name=} {self.face_name=}"]) + return {"CANCELLED"} + filepaths = face.filepaths for f in filepaths: butils.load_font_from_filepath(f) return {"FINISHED"} @@ -1402,6 +1404,9 @@ 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) + def invoke(self, context, event): wm = context.window_manager preferences = getPreferences(context) @@ -1425,30 +1430,95 @@ 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 - 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="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") + face : Font.FontFace = Font.get_font_face(font_name, face_name) + if face is not None: + loaded_glyphs = sorted(face.loaded_glyphs) + n = 16 + n_rows = int(len(loaded_glyphs) / n) + box = layout.box() + box.row().label(text="Glyphs to be exported:") + subbox = box.box() + for i in range(0, n_rows + 1): + text = "".join( + [ + f"{u}" + for ui, u in enumerate(loaded_glyphs) + if ui < (i + 1) * n and ui >= i * n + ] + ) + scale_y = 0.5 + row = subbox.row() + row.scale_y = scale_y + row.label(text=text) + row = layout.row() + export_dir = butils.bpy_to_abspath(abc3d_data.export_dir) + if os.access(export_dir, os.W_OK): + self.can_execute = True + elif os.path.exists(export_dir): + self.can_execute = False + row.alert = True + row.label(text="Export directory exists but is not writable") + row = layout.row() + row.alert = True + row.label(text="Please select another directory") + row = layout.row() + row.alert = True + elif not utils.can_create_path(export_dir): # does not exist and cannot be created + self.can_execute = False + row.alert = True + row.label(text="Directory does not exist and cannot be created") + row = layout.row() + row.alert = True + row.label(text="Please select another directory") + row = layout.row() + row.alert = True + elif utils.can_create_path(export_dir): # does not exist and can be created + self.can_execute = True + row.label(text="Directory does not exist") + row = layout.row() + row.label(text="But can and will be created on export") + row = layout.row() + else: + self.can_execute = False + row.alert = True + row.label(text="Please select another directory") + row = layout.row() + row.alert = True + + row.prop(abc3d_data, "export_dir") + else: + print(f"{utils.prefix()}::save_font_to_file ERROR {face=} {font_name=} {face_name=}") + print(f"{utils.prefix()} {Font.fonts=}") def execute(self, context): global shared scene = bpy.context.scene abc3d_data = scene.abc3d_data + if not self.can_execute: + butils.ShowMessageBox( + "Cannot export font", + "ERROR", + [ + f"export directory '{abc3d_data.export_dir}' does not exist or is not writable", + "try setting another path" + ] + ) + return {'CANCELLED'} + + if not os.path.exists(butils.bpy_to_abspath(abc3d_data.export_dir)): + path = butils.bpy_to_abspath(abc3d_data.export_dir) + if utils.can_create_path(path): + os.makedirs(path, exist_ok=True) + else: + butils.ShowMessageBox( + "Cannot export font", + "ERROR", + [ + f"export directory '{abc3d_data.export_dir}' does not exist and cannot be created", + "try setting another path" + ] + ) + return {'CANCELLED'} fontcollection = bpy.data.collections.get("ABC3D") @@ -1528,7 +1598,10 @@ class ABC3D_OT_SaveFontToFile(bpy.types.Operator): use_selection=True, use_active_scene=True, ) - bpy.app.timers.register(lambda: bpy.ops.scene.delete(), first_interval=1) + def delete_scene(): + bpy.ops.scene.delete() + return None + bpy.app.timers.register(lambda: delete_scene(), first_interval=1) # bpy.ops.scene.delete() # restore() @@ -1669,9 +1742,6 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): 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: @@ -1842,7 +1912,7 @@ def load_used_glyphs(): abc3d_data = scene.abc3d_data for t in abc3d_data.available_texts: a = Font.test_availability(t.font_name, t.face_name, t.text) - if type(a) == type(int()): + if isinstance(a, int): if a == Font.MISSING_FONT: butils.ShowMessageBox( "Missing Font", @@ -1859,9 +1929,9 @@ def load_used_glyphs(): "Do you have it installed?", ], ) - elif len(a["maybe"]) > 0: - for fp in a["filepaths"]: - butils.load_font_from_filepath(fp, a["maybe"]) + elif len(a.unloaded) > 0: + for fp in a.filepaths: + butils.load_font_from_filepath(fp, a.unloaded) @persistent diff --git a/butils.py b/butils.py index 138ed9f..a8a9a14 100644 --- a/butils.py +++ b/butils.py @@ -515,7 +515,11 @@ def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""): for mff in modified_font_faces: mff_glyphs = [] - face = Font.fonts[mff["font_name"]].faces[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.") + continue # iterate glyphs for g in face.glyphs: # iterate alternates @@ -543,17 +547,18 @@ def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""): def update_available_fonts(): abc3d_data = bpy.context.scene.abc3d_data - for font_name in Font.fonts.keys(): - for face_name in Font.fonts[font_name].faces.keys(): - found = False - for f in abc3d_data.available_fonts.values(): - if font_name == f.font_name and face_name == f.face_name: - found = True - 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}") + for font_and_face in Font.get_loaded_fonts_and_faces(): + found = False + font_name = font_and_face[0] + face_name = font_and_face[1] + for f in abc3d_data.available_fonts.values(): + if font_name == f.font_name and face_name == f.face_name: + found = True + 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}") # def update_available_texts(): @@ -754,21 +759,28 @@ def get_glyph_height(glyph_obj): def prepare_text(font_name, face_name, text, allow_replacement=True): - loaded, missing, loadable, files = Font.test_glyphs_availability( + 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") + if availability is Font.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 - if len(missing) > 0 and allow_replacement: + if len(availability.missing) > 0 and allow_replacement: replacement_search = "" - for m in missing: + for m in availability.missing: if m.isalpha(): replacement_search += m.swapcase() r = Font.test_availability(font_name, face_name, replacement_search) - loadable += r["maybe"] + loadable += r.unloaded # not update (loaded, missing, files), we only use loadable/maybe later if len(loadable) > 0: - for filepath in files: + for filepath in availability.filepaths: load_font_from_filepath(filepath, loadable, font_name, face_name) return True @@ -776,9 +788,9 @@ 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()) t_text = text_properties.text - for c in availability["missing"]: + for c in availability.missing: t_text = t_text.replace(c, "") - for c in AVAILABILITY["missing"]: + for c in AVAILABILITY.missing: t_text = t_text.replace(c, "") return t_text @@ -1016,14 +1028,8 @@ def transfer_text_object_to_text_properties(text_object, text_properties, id_fro if text == text_properties.text: is_good_text = True else: - 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, "") - for c in AVAILABILITY["missing"]: - t_text = t_text.replace(c, "") - if len(t_text) == len(text): + t_text = predict_actual_text(text_properties) + if t_text == text: is_good_text = True if is_good_text: text_properties.actual_text = text @@ -1252,7 +1258,7 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, actual_text = "" for i, c in enumerate(text_properties.text): - face = Font.fonts[text_properties.font_name].faces[text_properties.face_name] + face = Font.get_font_face(text_properties.font_name, text_properties.face_name) scalor = face.unit_factor * text_properties.font_size if c == "\\": is_command = True diff --git a/common/Font.py b/common/Font.py index e7a7ac0..4d0c6a7 100644 --- a/common/Font.py +++ b/common/Font.py @@ -1,5 +1,6 @@ from typing import Dict from pathlib import Path +from typing import NamedTuple # convenience dictionary for translating names to glyph ids # note: overwritten/extended by the content of "glypNamesToUnicode.txt" @@ -160,6 +161,7 @@ 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({}) @@ -177,6 +179,34 @@ def register_font(font_name, face_name, glyphs_in_fontfile, filepath): fonts[font_name].faces[face_name].filepaths.append(filepath) +def get_font(font_name): + if not fonts.keys().__contains__(font_name): + print(f"ABC3D::get_font: font name({font_name}) not found") + print(fonts.keys()) + return None + return fonts[font_name] + + +def get_font_face(font_name, face_name): + font = get_font(font_name) + if font is None: + return None + if not font.faces.keys().__contains__(face_name): + print( + f"ABC3D::get_font_face (font: {font_name}): face name({face_name}) not found" + ) + print(font.faces.keys()) + return None + return font.faces[face_name] + + +def get_font_face_filepaths(font_name, face_name): + face = get_font_face(font_name, face_name) + if not face: + return None + return face.filepaths + + def add_glyph(font_name, face_name, glyph_id, glyph_object): """add_glyph adds a glyph to a FontFace it creates the :class:`Font` and :class:`FontFace` if it does not exist yet @@ -203,6 +233,38 @@ def add_glyph(font_name, face_name, glyph_id, glyph_object): fonts[font_name].faces[face_name].loaded_glyphs.append(glyph_id) +def get_glyphs(font_name, face_name, glyph_id): + """get_glyphs returns an array of glyphs of a FontFace + + :param font_name: The :class:`Font` you want to get the glyph from + :type font_name: str + :param face_name: The :class:`FontFace` you want to get the glyph from + :type face_name: str + :param glyph_id: The ``glyph_id`` from the glyph you want + :type glyph_id: str + ... + :return: returns a list of the glyph objects, or an empty list if none exists + :rtype: `List` + """ + + 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()) + return [] + + glyphs_for_id = face.glyphs.get(glyph_id) + if glyphs_for_id is None: + print( + f"ABC3D::get_glyph: font({font_name}) face({face_name}) glyph({glyph_id}) 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 [] + + return glyphs_for_id + + def get_glyph(font_name, face_name, glyph_id, alternate=0): """add_glyph adds a glyph to a FontFace it creates the :class:`Font` and :class:`FontFace` if it does not exist yet @@ -218,25 +280,22 @@ def get_glyph(font_name, face_name, glyph_id, alternate=0): :rtype: `Object` """ - if not fonts.keys().__contains__(font_name): - # print(f"ABC3D::get_glyph: font name({font_name}) not found") - # print(fonts.keys()) + glyphs = get_glyphs(font_name, face_name, glyph_id) + + if len(glyphs) <= alternate or len(glyphs) == 0: + print( + f"ABC3D::get_glyph: font({font_name}) face({face_name}) glyph({glyph_id})[{alternate}] not found" + ) return None - face = fonts[font_name].faces.get(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()) - return None + return glyphs[alternate] - glyphs_for_id = face.glyphs.get(glyph_id) - if glyphs_for_id is 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] +class GlyphsAvailability(NamedTuple): + loaded: str + missing: str + unloaded: str + filepaths: list[str] def test_glyphs_availability(font_name, face_name, text): @@ -245,24 +304,24 @@ def test_glyphs_availability(font_name, face_name, text): not fonts.keys().__contains__(font_name) or fonts[font_name].faces.get(face_name) is None ): - return "", "", text # , , + return GlyphsAvailability("", "", "", []) loaded = [] missing = [] - maybe = [] + unloaded = [] 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) + unloaded.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 ( + return GlyphsAvailability( "".join(loaded), "".join(missing), - "".join(maybe), + "".join(unloaded), fonts[font_name].faces[face_name].filepaths, ) @@ -288,15 +347,10 @@ def test_availability(font_name, face_name, text): return MISSING_FONT if fonts[font_name].faces.get(face_name) is None: return MISSING_FACE - loaded, missing, maybe, filepaths = test_glyphs_availability( + availability: GlyphsAvailability = test_glyphs_availability( font_name, face_name, text ) - return { - "loaded": loaded, - "missing": missing, - "maybe": maybe, - "filepaths": filepaths, - } + return availability # holds all fonts diff --git a/common/utils.py b/common/utils.py index 796d3e6..d7f3fab 100644 --- a/common/utils.py +++ b/common/utils.py @@ -89,6 +89,26 @@ def printerr(*args, **kwargs): def removeNonAlphabetic(s): return "".join([i for i in s if i.isalpha()]) +import pathlib +import os + +def can_create_path(path_str : str): + path = pathlib.Path(path_str).absolute().resolve() + + while True: # this looks dangerours, but it actually is not + if path.exists(): + if os.access(path, os.W_OK): + return True + else: + return False + elif path == path.parent: + # should never be reached, because root exists + # but if it doesn't.. well then we can't + return False + + path = path.parent + + # # Evaluate a bezier curve for the parameter 0<=t<=1 along its length # def evaluateBezierPoint(p1, h1, h2, p2, t):