From cd6457352b78d2f35a34b85e8050288c0fe25bc3 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sat, 16 Nov 2024 15:13:34 +0100 Subject: [PATCH 001/103] fix updater zipball --- addon_updater.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addon_updater.py b/addon_updater.py index f1ac048..290da12 100644 --- a/addon_updater.py +++ b/addon_updater.py @@ -1775,7 +1775,7 @@ class ForgejoEngine: return [ { "name": tag["name"], - "zipball_url": self.get_zip_url(tag["commit"]["id"], updater) + "zipball_url": self.get_zip_url(tag["commit"]["sha"], updater) } for tag in response] From 2ba83ea3feadae430ea67c2389e26a17d08ce429 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Thu, 21 Nov 2024 14:32:29 +0100 Subject: [PATCH 002/103] updater - introduce updater.host - allow dev branch - minor improvements --- addon_updater.py | 6 +++++- addon_updater_ops.py | 10 +++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/addon_updater.py b/addon_updater.py index 290da12..ca9e6d1 100644 --- a/addon_updater.py +++ b/addon_updater.py @@ -1740,11 +1740,15 @@ class ForgejoEngine: """Integration to Forgejo/Gitea API""" def __init__(self): - self.api_url = 'https://git.pointer.click' + # the api_url may be overwritten by form_repo_url + # if updater.host is set + self.api_url = 'https://codeberg.org' self.token = None self.name = "forgejo" def form_repo_url(self, updater): + if updater.host: + self.api_url = "https://" + updater.host return "{}/api/v1/repos/{}/{}".format(self.api_url, updater.user, updater.repo) def form_tags_url(self, updater): diff --git a/addon_updater_ops.py b/addon_updater_ops.py index 6cc021d..0c3e108 100644 --- a/addon_updater_ops.py +++ b/addon_updater_ops.py @@ -72,7 +72,7 @@ except Exception as e: # not match and have errors. Must be all lowercase and no spaces! Should also # be unique among any other addons that could exist (using this updater code), # to avoid clashes in operator registration. -updater.addon = "addon_updater_demo" +updater.addon = "abc3d" # ----------------------------------------------------------------------------- @@ -1346,6 +1346,10 @@ def register(bl_info): # updater.engine = "GitLab" # updater.engine = "Bitbucket" + # set your host. + # only Forgejo. (codeberg.org is default if not specified). + updater.host = "git.pointer.click" + # If using private repository, indicate the token here. # Must be set after assigning the engine. # **WARNING** Depending on the engine, this token can act like a password!! @@ -1392,7 +1396,7 @@ def register(bl_info): updater.backup_current = True # True by default # Sample ignore patterns for when creating backup of current during update. - updater.backup_ignore_patterns = ["__pycache__"] + updater.backup_ignore_patterns = [".git", "__pycache__", "*.bat", ".gitignore", "*.exe"] # Alternate example patterns: # updater.backup_ignore_patterns = [".git", "__pycache__", "*.bat", ".gitignore", "*.exe"] @@ -1461,7 +1465,7 @@ def register(bl_info): # Note: updater.include_branch_list defaults to ['master'] branch if set to # none. Example targeting another multiple branches allowed to pull from: # updater.include_branch_list = ['master', 'dev'] - updater.include_branch_list = ['main'] # None is the equivalent = ['master'] + updater.include_branch_list = ['main', 'dev'] # None is the equivalent = ['master'] # Only allow manual install, thus prompting the user to open # the addon's web page to download, specifically: updater.website From 5c79392b403d9eb51204d0c2880b68606e9fd18c Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Thu, 21 Nov 2024 14:35:21 +0100 Subject: [PATCH 003/103] more robust selection getter this could actually be a function in butils --- butils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/butils.py b/butils.py index c6737be..00e5b14 100644 --- a/butils.py +++ b/butils.py @@ -713,7 +713,7 @@ def set_text_on_curve(text_properties, recursive=True): text_properties.glyphs.clear() #TODO: fix selection with context_override - previous_selection = bpy.context.selected_objects + previous_selection = bpy.context.selected_objects if hasattr(bpy.context, "selected_objects") else [ o for o in bpy.context.scene.objects if o.select_get() ] bpy.ops.object.select_all(action='DESELECT') selected_objects = [] From c3055ac2c9291c13b95f7cb941757ed18f32975a Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Thu, 21 Nov 2024 14:44:51 +0100 Subject: [PATCH 004/103] update doc --- CONTRIBUTING.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ README.md | 40 ++-------------------------------------- 2 files changed, 49 insertions(+), 38 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f67ccf4 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,47 @@ +``` + _ ____ ____ _____ ____ + / \ | __ ) / ___|___ /| _ \ + / _ \ | _ \| | |_ \| | | | + / ___ \| |_) | |___ ___) | |_| | +/_/ \_\____/ \____|____/|____/ +``` + +Convenience tool to work with 3D typography in Blender and Cinema4D. + +# get bpy python working by: +```bash +$HOME/git/tools/blender_git/build_linux_v4.1/bin/4.1/python/bin/python3.11 -m venv venv +source venv/bin/activate +pip install bpy +``` + +to install mathutils, this was necessary for me: +``` +sudo xbps-install -Sy python3.11-devel +CFLAGS=$(python3.11-config --cflags) LDFLAGS=$(python3.11-config --ldflags) pip install mathutils +``` + +# install addon: +```bash +cd +ln -s $(pwd) $HOME/git/tools/blender_git/build_linux_v4.1/bin/4.1/scripts/addons/abc3d +``` + +# get blender addon path: +```python +bpy.utils.script_paths() +``` +then check it for the `addons` directory + +# addons dir: +``` +~/git/tools/blender_git/build_linux_v4.1/bin/4.1/scripts/addons/ +``` + +# addon data: +``` +~/.config/blender/4.1/datafiles +``` + +# reload addon in blender: +F3 -> "reload scripts" diff --git a/README.md b/README.md index 451ac98..9b5d420 100644 --- a/README.md +++ b/README.md @@ -8,42 +8,6 @@ Convenience tool to work with 3D typography in Blender and Cinema4D. -The readme is at the moment for development only. Install as you would normally install an addon. +Install as you would normally install an addon. -# get bpy python working by: -```bash -$HOME/git/tools/blender_git/build_linux_v4.1/bin/4.1/python/bin/python3.11 -m venv venv -source venv/bin/activate -pip install bpy -``` - -to install mathutils, this was necessary for me: -``` -sudo xbps-install -Sy python3.11-devel -CFLAGS=$(python3.11-config --cflags) LDFLAGS=$(python3.11-config --ldflags) pip install mathutils -``` - -# install addon: -```bash -cd -ln -s $(pwd) $HOME/git/tools/blender_git/build_linux_v4.1/bin/4.1/scripts/addons/abc3d -``` - -# get blender addon path: -```python -bpy.utils.script_paths() -``` -then check it for the `addons` directory - -# addons dir: -``` -~/git/tools/blender_git/build_linux_v4.1/bin/4.1/scripts/addons/ -``` - -# addon data: -``` -~/.config/blender/4.1/datafiles -``` - -# reload addon in blender: -F3 -> "reload scripts" +Instructions for development in [CONTRIBUTING,md](./CONTRIBUTING.md). From 7e2eeeeec14c824d40549cc99ebb2d7eea873c22 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Thu, 21 Nov 2024 14:59:26 +0100 Subject: [PATCH 005/103] remove right panel --- __init__.py | 60 ----------------------------------------------------- 1 file changed, 60 deletions(-) diff --git a/__init__.py b/__init__.py index c8575d2..0943139 100644 --- a/__init__.py +++ b/__init__.py @@ -1417,65 +1417,6 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): return {'FINISHED'} -class ABC3D_PT_RightPropertiesPanel(bpy.types.Panel): - """Creates a Panel in the Object properties window""" - bl_label = f"{bl_info['name']}" - bl_idname = "ABC3D_PT_RightPropertiesPanel" - bl_space_type = 'PROPERTIES' - bl_region_type = 'WINDOW' - bl_context = "object" - - @classmethod - def poll(self, context): - # only show the panel, if it's a textobject or a glyph - is_text = type(next((t for t in context.scene.abc3d_data.available_texts if t.text_object == - context.active_object), None)) != type(None) - is_glyph = type(next((t for t in context.scene.abc3d_data.available_texts if t.text_object == - context.active_object.parent), None)) != type(None) - return is_text or is_glyph - - def draw(self, context): - layout = self.layout - scene = context.scene - abc3d_data = scene.abc3d_data - - obj = context.active_object - - def is_it_text(): - return type(next((t for t in context.scene.abc3d_data.available_texts if t.text_object == context.active_object), None)) != type(None) - - def is_it_glyph(): - return type(next((t for t in context.scene.abc3d_data.available_texts if t.text_object == context.active_object.parent), None)) != type(None) - - is_text = is_it_text() - is_glyph = is_it_glyph() - - textobject = obj if is_text else obj.parent if is_glyph else obj - available_text = abc3d_data.available_texts[abc3d_data.active_text_index] - - # row = layout.row() - # row.label(text="Hello world!", icon='WORLD_DATA') - # row = layout.row() - # row.label(text="Active object is: " + obj.name) - # row = layout.row() - # row.label(text="text object is: " + textobject.name) - row = layout.row() - row.label(text=f"active text index is: {abc3d_data.active_text_index}") - - layout.row().label(text="Text Properties:") - layout.row().prop(available_text, "text") - layout.row().prop(available_text, "letter_spacing") - layout.row().prop(available_text, "font_size") - layout.row().prop(available_text, "offset") - layout.row().prop(available_text, "compensate_curvature") - layout.row().prop(available_text, "ignore_orientation") - layout.column().prop(available_text, "translation") - layout.column().prop(available_text, "orientation") - - if is_glyph: - layout.row().label(text="Glyph Properties:") - - class ABC3D_OT_Reporter(bpy.types.Operator): bl_idname = f"{__name__}.reporter" bl_label = "Report" @@ -1528,7 +1469,6 @@ classes = ( ABC3D_OT_ToggleABC3DCollection, ABC3D_OT_SaveFontToFile, ABC3D_OT_CreateFontFromObjects, - ABC3D_PT_RightPropertiesPanel, ABC3D_OT_Reporter, ) From a2c4ba60f26a7b4cebbd3fb84647f620e45a36dd Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Thu, 21 Nov 2024 15:04:08 +0100 Subject: [PATCH 006/103] cleanup --- __init__.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/__init__.py b/__init__.py index 0943139..935fc2b 100644 --- a/__init__.py +++ b/__init__.py @@ -311,7 +311,6 @@ class ABC3D_UL_texts(bpy.types.UIList): def invoke(self, context, event): pass - class ABC3D_PT_Panel(bpy.types.Panel): bl_label = f"{__name__} panel" bl_category = "ABC3D" @@ -333,27 +332,6 @@ class ABC3D_PT_Panel(bpy.types.Panel): text="open asset directory", icon='FILEBROWSER') -# class ABC3D_PT_LoadFontPanel(bpy.types.Panel): - # bl_label = "Install a new font" - # bl_parent_id = "ABC3D_PT_Panel" - # bl_category = "ABC3D" - # bl_space_type = "VIEW_3D" - # bl_region_type = "UI" - - # def draw(self, context): - # layout = self.layout - # wm = context.window_manager - # scene = context.scene - - # abc3d_data = scene.abc3d_data - - # box = layout.box() - # box.row().label(text="1. Select fontfile") - # box.row().prop(context.scene.abc3d_data, "font_path") - # box.row().label(text="2. Install it:") - # box.row().operator(f"{__name__}.install_font", text='Install new font') - - class ABC3D_PT_FontList(bpy.types.Panel): bl_label = "Font List" bl_parent_id = "ABC3D_PT_Panel" @@ -1449,7 +1427,6 @@ classes = ( ABC3D_UL_fonts, ABC3D_UL_texts, ABC3D_PT_Panel, - # ABC3D_PT_LoadFontPanel, ABC3D_PT_FontList, ABC3D_PT_TextPlacement, ABC3D_PT_TextManagement, From 35d864b9b8876fc56efd65801245b0f6466f6f76 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sat, 7 Dec 2024 14:28:37 +0100 Subject: [PATCH 007/103] remove outdated comments --- common/Font.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/common/Font.py b/common/Font.py index ef34499..aa936db 100644 --- a/common/Font.py +++ b/common/Font.py @@ -120,9 +120,6 @@ class Font: self.faces = faces -# 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({}) From 42c4a338011206cf840977ea69ecbc955326f9c1 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sat, 7 Dec 2024 14:57:33 +0100 Subject: [PATCH 008/103] manual positioning closes #1 --- __init__.py | 35 +++++--- addon_updater.py | 1 - butils.py | 204 +++++++++++++++++++++++++++++++++-------------- 3 files changed, 168 insertions(+), 72 deletions(-) diff --git a/__init__.py b/__init__.py index 935fc2b..5e40091 100644 --- a/__init__.py +++ b/__init__.py @@ -383,10 +383,10 @@ class ABC3D_PT_FontList(bpy.types.Panel): row.scale_y = scale_y row.label(text=text) row = layout.row() - oper = row.operator(f"{__name__}.load_font", + oper_lf = row.operator(f"{__name__}.load_font", text='Load all glyphs in memory') - oper.font_name = font_name - oper.face_name = face_name + oper_lf.font_name = font_name + oper_lf.face_name = face_name class ABC3D_PT_TextPlacement(bpy.types.Panel): @@ -914,6 +914,7 @@ class ABC3D_OT_RemoveText(bpy.types.Operator): def delif(o, p): if p in o: del o[p] + delif(mom, f"{utils.prefix()}_type") delif(mom, f"{utils.prefix()}_linked_textobject") delif(mom, f"{utils.prefix()}_font_name") delif(mom, f"{utils.prefix()}_face_name") @@ -1046,7 +1047,7 @@ class ABC3D_OT_PlaceText(bpy.types.Operator): # t.text) # or this: # butils.set_text_on_curve(t) - # else: + else: butils.ShowMessageBox( title="No object selected", message=( @@ -1372,7 +1373,7 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): font_name, face_name, glyph_id, - o) + bpy.types.PointerProperty(o)) # TODO: is there a better way to iterate over a CollectionProperty? found = False @@ -1484,7 +1485,6 @@ def detect_text(): def load_used_glyphs(): - print("LOAD USED GLYPHS") scene = bpy.context.scene abc3d_data = scene.abc3d_data for t in abc3d_data.available_texts: @@ -1528,19 +1528,33 @@ def on_frame_changed(self, dummy): # TODO PERFORMANCE: only on demand butils.set_text_on_curve(t) +depsgraph_updates_locked = False +def unlock_depsgraph_updates(): + global depsgraph_updates_locked + depsgraph_updates_locked = False + +def lock_depsgraph_updates(): + global depsgraph_updates_locked + depsgraph_updates_locked = True + if bpy.app.timers.is_registered(unlock_depsgraph_updates): + bpy.app.timers.unregister(unlock_depsgraph_updates) + bpy.app.timers.register(unlock_depsgraph_updates, first_interval=1) + +import time @persistent def on_depsgraph_update(scene, depsgraph): - if not bpy.context.mode.startswith("EDIT"): + global depsgraph_updates_locked + if not bpy.context.mode.startswith("EDIT") and not depsgraph_updates_locked: for u in depsgraph.updates: if f"{utils.prefix()}_linked_textobject" in u.id.keys() \ and f"{utils.prefix()}_type" in u.id.keys() \ and u.id[f"{utils.prefix()}_type"] == 'textobject': linked_textobject = u.id[f"{utils.prefix()}_linked_textobject"] if u.is_updated_geometry and len(scene.abc3d_data.available_texts) > linked_textobject: + lock_depsgraph_updates() def later(): if not "lock_depsgraph_update_ntimes" in scene.abc3d_data \ - or scene.abc3d_data["lock_depsgraph_update_ntimes"] == 0: - print("******* not yet") + or scene.abc3d_data["lock_depsgraph_update_ntimes"] <= 0: butils.set_text_on_curve( scene.abc3d_data.available_texts[linked_textobject]) elif scene.abc3d_data["lock_depsgraph_update_ntimes"] > 0: @@ -1596,6 +1610,9 @@ def unregister(): if on_frame_changed in bpy.app.handlers.frame_change_post: bpy.app.handlers.frame_change_post.remove(on_frame_changed) + if on_depsgraph_update in bpy.app.handlers.depsgraph_update_post: + bpy.app.handlers.depsgraph_update_post.remove(on_depsgraph_update) + del bpy.types.Scene.abc3d_data print(f"UNREGISTER {bl_info['name']}") diff --git a/addon_updater.py b/addon_updater.py index ca9e6d1..3ca5a3e 100644 --- a/addon_updater.py +++ b/addon_updater.py @@ -1782,7 +1782,6 @@ class ForgejoEngine: "zipball_url": self.get_zip_url(tag["commit"]["sha"], updater) } for tag in response] - # ----------------------------------------------------------------------------- # The module-shared class instance, # should be what's imported to other files diff --git a/butils.py b/butils.py index 00e5b14..b3b5e00 100644 --- a/butils.py +++ b/butils.py @@ -1,11 +1,12 @@ -import bpy -import mathutils -import queue import importlib import os +import queue import re from multiprocessing import Process +import bpy +import mathutils + # import time # for debugging performance # then import dependencies for our addon @@ -111,7 +112,8 @@ def calc_tangent_on_bezier(bezier_point_1, bezier_point_2, t): (-3 * (t**2) + 6 * t * (1 - t)) * h2 + (3 * t**2) * p2 ).normalized() -from math import radians, sqrt, pi, acos +from math import acos, pi, radians, sqrt + def align_rotations_auto_pivot(mask, input_rotations, vectors, factors, local_main_axis): output_rotations = [mathutils.Matrix().to_3x3() for _ in range(len(input_rotations))] @@ -449,6 +451,7 @@ def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""): glyph_obj = move_in_fontcollection( o, fontcollection) + glyph_obj_pointer = bpy.types.PointerProperty(glyph_obj) if glyph_obj == o: del o[marker_property] @@ -457,7 +460,7 @@ def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""): font_name, face_name, glyph_id, - glyph_obj) + glyph_obj_pointer) for c in o.children: if is_metrics_object(c): add_metrics_obj_from_bound_box(glyph_obj, @@ -472,7 +475,7 @@ def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""): for g in face.glyphs: # iterate alternates for glyph in face.glyphs[g]: - glyphs.append(glyph) + glyphs.append(get_original(glyph)) if len(glyphs) > 0: add_default_metrics_to_objects(glyphs) # calculate unit factor @@ -586,18 +589,19 @@ def ShowMessageBox(title = "Message Box", icon = 'INFO', message=""): bpy.context.window_manager.popup_menu(draw, title = title, icon = icon) 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() + completely_delete_objects(objs) -def completely_delete_objects(objs): - simply_delete_objects(objs) - - # remove deleted objects - # this is necessary +def completely_delete_objects(objs, recursive=True): for g in objs: if type(g) != type(None): + if recursive: + try: + if hasattr(g, "children") and len(g.children) > 0: + completely_delete_objects(g.children) + except ReferenceError as e: + # not important + pass + try: bpy.data.objects.remove(g, do_unlink=True) except ReferenceError as e: @@ -672,51 +676,80 @@ def prepare_text(font_name, face_name, text): return True def is_bezier(curve): + if curve.type != 'CURVE': + return False if len(curve.data.splines) < 1: return False - return curve.data.splines[0].type == 'BEZIER' + for spline in curve.data.splines: + if spline.type != 'BEZIER': + return False + return True + +def will_regenerate(text_properties): + mom = text_properties.text_object + + if len(text_properties.text) != len(text_properties.glyphs): + return True + + for i, g in enumerate(text_properties.glyphs): + if not hasattr(g.glyph_object, "type"): + return True + elif g.glyph_object.type != 'EMPTY': + return True + # check if perhaps one glyph was deleted + elif type(g.glyph_object) == type(None): + return True + elif type(g.glyph_object.parent) == type(None): + return True + elif g.glyph_object.parent.users_collection != g.glyph_object.users_collection: + return True + elif len(text_properties.text) > i and g.glyph_id != text_properties.text[i]: + return True + elif len(text_properties.text) > i and (g.glyph_object[f"{utils.prefix()}_font_name"] != text_properties.font_name + or g.glyph_object[f"{utils.prefix()}_face_name"] != text_properties.face_name): + return True + + return False + + +def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4): + """set_text_on_curve + + An earlier reset cancels the other. + To disable reset, set both to false. + + :param text_properties: all information necessary to set text on a curve + :type text_properties: ABC3D_text_properties + :param reset_timeout_s: reset external parameters after timeout. (<= 0) = immediate, (> 0) = non-blocking reset timeout in seconds, (False) = no timeout reset + :type reset_timeout_s: float + :param reset_depsgraph_n: reset external parameters after n-th depsgraph update. (<= 0) = immediate, (> 0) = reset after n-th depsgraph update, (False) = no depsgraph reset + :type reset_depsgraph_n: int + """ -def set_text_on_curve(text_properties, recursive=True): # starttime = time.perf_counter_ns() mom = text_properties.text_object if mom.type != "CURVE": return False - regenerate = False - glyph_objects = [] - for i, g in enumerate(text_properties.glyphs): - glyph_objects.append(g.glyph_object) + distribution_type = 'CALCULATE' if is_bezier(mom) else 'FOLLOW_PATH' - # check if perhaps one glyph was deleted - if (type(g.glyph_object) == type(None) - or type(g.glyph_object.parent) == type(None) - or g.glyph_object.parent.users_collection != g.glyph_object.users_collection): - regenerate = True - elif len(text_properties.text) > i and g.glyph_id != text_properties.text[i]: - regenerate = True - elif len(text_properties.text) > i and (g.glyph_object[f"{utils.prefix()}_font_name"] != text_properties.font_name - or g.glyph_object[f"{utils.prefix()}_face_name"] != text_properties.face_name): - regenerate = True - - if len(text_properties.text) != len(text_properties.glyphs): - regenerate = True - - # blender bug + # use_path messes with parenting + # however, we need it for follow_path # https://projects.blender.org/blender/blender/issues/100661 - if mom.data.use_path: - regenerate = True + previous_use_path = mom.data.use_path + if distribution_type == 'CALCULATE': + mom.data.use_path = False + elif distribution_type == 'FOLLOW_PATH': + mom.data.use_path = True + + regenerate = will_regenerate(text_properties) # if we regenerate.... delete objects - if regenerate: - completely_delete_objects(glyph_objects) - + if regenerate and text_properties.get("glyphs"): + glyph_objects = [ g["glyph_object"] for g in text_properties["glyphs"] ] + completely_delete_objects(glyph_objects, True) text_properties.glyphs.clear() -#TODO: fix selection with context_override - previous_selection = bpy.context.selected_objects if hasattr(bpy.context, "selected_objects") else [ o for o in bpy.context.scene.objects if o.select_get() ] - bpy.ops.object.select_all(action='DESELECT') - selected_objects = [] - curve_length = get_curve_length(mom) advance = text_properties.offset glyph_advance = 0 @@ -747,7 +780,7 @@ def set_text_on_curve(text_properties, recursive=True): glyph = Font.get_glyph(text_properties.font_name, text_properties.face_name, - glyph_id) + glyph_id).original if glyph == None: # self.report({'ERROR'}, f"Glyph not found for {font_name} {face_name} {glyph_id}") @@ -755,16 +788,20 @@ def set_text_on_curve(text_properties, recursive=True): continue ob = None + obg = None if regenerate: - ob = bpy.data.objects.new(f"{glyph_id}", glyph.data) + ob = bpy.data.objects.new(f"{glyph_id}", None) + obg = bpy.data.objects.new(f"{glyph_id}_mesh", glyph.data) ob[f"{utils.prefix()}_type"] = "glyph" ob[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id ob[f"{utils.prefix()}_font_name"] = text_properties.font_name ob[f"{utils.prefix()}_face_name"] = text_properties.face_name else: ob = text_properties.glyphs[i].glyph_object + for c in ob.children: + if c.name.startswith(f"{glyph_id}_mesh"): + obg = c - distribution_type = 'CALCULATE' if is_bezier(mom) else 'FOLLOW_PATH' if distribution_type == 'FOLLOW_PATH': ob.constraints.new(type='FOLLOW_PATH') ob.constraints["Follow Path"].target = mom @@ -773,6 +810,7 @@ def set_text_on_curve(text_properties, recursive=True): ob.constraints["Follow Path"].use_curve_follow = True ob.constraints["Follow Path"].forward_axis = "FORWARD_X" ob.constraints["Follow Path"].up_axis = "UP_Y" + spline_index = 0 elif distribution_type == 'CALCULATE': location, tangent, spline_index = calc_point_on_bezier_curve(mom, advance, True, True) if spline_index != previous_spline_index: @@ -780,6 +818,11 @@ def set_text_on_curve(text_properties, recursive=True): if regenerate: ob.location = mom.matrix_world @ (location + text_properties.translation) + mom.users_collection[0].objects.link(obg) + mom.users_collection[0].objects.link(ob) + ob.parent = mom + obg.parent = ob + obg.location = mathutils.Vector((0.0, 0.0, 0.0)) else: ob.location = (location + text_properties.translation) @@ -796,16 +839,20 @@ def set_text_on_curve(text_properties, recursive=True): local_main_axis) if ob.rotation_mode != 'QUATERNION': ob.rotation_mode = 'QUATERNION' + if obg.rotation_mode != 'QUATERNION': + obg.rotation_mode = 'QUATERNION' q = mathutils.Quaternion() q.rotate(text_properties.orientation) if regenerate: - ob.rotation_quaternion = (mom.matrix_world @ motor[0] @ q.to_matrix().to_4x4()).to_quaternion() + obg.rotation_quaternion = q + ob.rotation_quaternion = (mom.matrix_world @ motor[0]).to_quaternion() else: - ob.rotation_quaternion = (motor[0] @ q.to_matrix().to_4x4()).to_quaternion() + ob.rotation_quaternion = motor[0].to_quaternion() else: q = mathutils.Quaternion() q.rotate(text_properties.orientation) - ob.rotation_quaternion = q + # obg.rotation_quaternion = q + obg.rotation_quaternion = (mom.matrix_world @ q.to_matrix().to_4x4()).to_quaternion() # ob.rotation_quaternion = (mom.matrix_world @ q.to_matrix().to_4x4()).to_quaternion() @@ -839,17 +886,12 @@ def set_text_on_curve(text_properties, recursive=True): previous_spline_index = spline_index if regenerate: - mom.users_collection[0].objects.link(ob) glyph_data = text_properties.glyphs.add() glyph_data.glyph_id = glyph_id glyph_data.glyph_object = ob glyph_data.letter_spacing = 0 - ob.select_set(True) - + if regenerate: - mom.select_set(True) - # https://projects.blender.org/blender/blender/issues/100661 - mom.data.use_path = False mom[f"{utils.prefix()}_type"] = "textobject" mom[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id mom[f"{utils.prefix()}_font_name"] = text_properties.font_name @@ -858,10 +900,42 @@ def set_text_on_curve(text_properties, recursive=True): mom[f"{utils.prefix()}_letter_spacing"] = text_properties.letter_spacing mom[f"{utils.prefix()}_orientation"] = text_properties.orientation mom[f"{utils.prefix()}_translation"] = text_properties.translation - bpy.context.view_layer.objects.active = mom - bpy.ops.object.parent_set(type='OBJECT') - bpy.context.scene.abc3d_data["lock_depsgraph_update_ntimes"] = len(bpy.context.selected_objects) - mom["lock_depsgraph_update_ntimes"] = len(bpy.context.selected_objects) + + if "lock_depsgraph_update_ntimes" in bpy.context.scene.abc3d_data: + bpy.context.scene.abc3d_data["lock_depsgraph_update_ntimes"] += len(bpy.context.selected_objects) + else: + bpy.context.scene.abc3d_data["lock_depsgraph_update_ntimes"] = len(bpy.context.selected_objects) + + # NOTE: we reset with a timeout, as setting and resetting certain things + # in fast succession will cause visual glitches (e.g. {}.data.use_path). + def reset(): + mom.data.use_path = previous_use_path + if counted_reset in bpy.app.handlers.depsgraph_update_post: + bpy.app.handlers.depsgraph_update_post.remove(counted_reset) + if bpy.app.timers.is_registered(reset): + bpy.app.timers.unregister(reset) + + molotov = reset_depsgraph_n + 0 + def counted_reset(scene, depsgraph): + nonlocal molotov + if molotov == 0: + reset() + else: + molotov -= 1 + + # unregister previous resets to avoid multiple execution + if bpy.app.timers.is_registered(reset): + bpy.app.timers.unregister(reset) + if counted_reset in bpy.app.handlers.depsgraph_update_post: + bpy.app.handlers.depsgraph_update_post.remove(counted_reset) + + if not isinstance(reset_timeout_s, bool): + if reset_timeout_s > 0: + bpy.app.timers.register(reset, first_interval=reset_timeout_s) + elif reset_timeout <= 0: + reset() + + bpy.app.handlers.depsgraph_update_post.append(counted_reset) # endtime = time.perf_counter_ns() # elapsedtime = endtime - starttime @@ -1095,6 +1169,12 @@ def get_metrics_object(o): return c return None +def get_original(o): + if hasattr(o, "original"): + return o.original + else: + return o + def add_default_metrics_to_objects(objects=None, overwrite_existing=False): if type(objects) == type(None): objects=bpy.context.selected_objects From 23624ea1ebac5a8693218964401ea0950840fa22 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sat, 7 Dec 2024 15:07:11 +0100 Subject: [PATCH 009/103] bump version v0.0.3 --- __init__.py | 3 ++- common/utils.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/__init__.py b/__init__.py index 5e40091..3fe519a 100644 --- a/__init__.py +++ b/__init__.py @@ -15,12 +15,13 @@ import importlib bl_info = { "name": "ABC3D", "author": "Jakob Schlötter, Studio Pointer*", - "version": (0, 0, 2), + "version": (0, 0, 3), "blender": (4, 1, 0), "location": "VIEW3D", "description": "Convenience addon for 3D fonts", "category": "Typography", } +# NOTE: also change version in common/utils.py # make sure that modules are reloadable # when registering diff --git a/common/utils.py b/common/utils.py index e9757b4..1e815e8 100644 --- a/common/utils.py +++ b/common/utils.py @@ -1,10 +1,10 @@ - +# NOTE: also change version in ../__init__.py def get_version_major(): return 0 def get_version_minor(): return 0 def get_version_patch(): - return 2 + return 3 def get_version_string(): return f"{get_version_major()}.{get_version_minor()}.{get_version_patch}" def prefix(): From bae49a234645a408e0d207a283ef12e571c94b39 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Thu, 21 Nov 2024 14:59:26 +0100 Subject: [PATCH 010/103] remove right panel --- __init__.py | 60 ----------------------------------------------------- 1 file changed, 60 deletions(-) diff --git a/__init__.py b/__init__.py index c8575d2..0943139 100644 --- a/__init__.py +++ b/__init__.py @@ -1417,65 +1417,6 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): return {'FINISHED'} -class ABC3D_PT_RightPropertiesPanel(bpy.types.Panel): - """Creates a Panel in the Object properties window""" - bl_label = f"{bl_info['name']}" - bl_idname = "ABC3D_PT_RightPropertiesPanel" - bl_space_type = 'PROPERTIES' - bl_region_type = 'WINDOW' - bl_context = "object" - - @classmethod - def poll(self, context): - # only show the panel, if it's a textobject or a glyph - is_text = type(next((t for t in context.scene.abc3d_data.available_texts if t.text_object == - context.active_object), None)) != type(None) - is_glyph = type(next((t for t in context.scene.abc3d_data.available_texts if t.text_object == - context.active_object.parent), None)) != type(None) - return is_text or is_glyph - - def draw(self, context): - layout = self.layout - scene = context.scene - abc3d_data = scene.abc3d_data - - obj = context.active_object - - def is_it_text(): - return type(next((t for t in context.scene.abc3d_data.available_texts if t.text_object == context.active_object), None)) != type(None) - - def is_it_glyph(): - return type(next((t for t in context.scene.abc3d_data.available_texts if t.text_object == context.active_object.parent), None)) != type(None) - - is_text = is_it_text() - is_glyph = is_it_glyph() - - textobject = obj if is_text else obj.parent if is_glyph else obj - available_text = abc3d_data.available_texts[abc3d_data.active_text_index] - - # row = layout.row() - # row.label(text="Hello world!", icon='WORLD_DATA') - # row = layout.row() - # row.label(text="Active object is: " + obj.name) - # row = layout.row() - # row.label(text="text object is: " + textobject.name) - row = layout.row() - row.label(text=f"active text index is: {abc3d_data.active_text_index}") - - layout.row().label(text="Text Properties:") - layout.row().prop(available_text, "text") - layout.row().prop(available_text, "letter_spacing") - layout.row().prop(available_text, "font_size") - layout.row().prop(available_text, "offset") - layout.row().prop(available_text, "compensate_curvature") - layout.row().prop(available_text, "ignore_orientation") - layout.column().prop(available_text, "translation") - layout.column().prop(available_text, "orientation") - - if is_glyph: - layout.row().label(text="Glyph Properties:") - - class ABC3D_OT_Reporter(bpy.types.Operator): bl_idname = f"{__name__}.reporter" bl_label = "Report" @@ -1528,7 +1469,6 @@ classes = ( ABC3D_OT_ToggleABC3DCollection, ABC3D_OT_SaveFontToFile, ABC3D_OT_CreateFontFromObjects, - ABC3D_PT_RightPropertiesPanel, ABC3D_OT_Reporter, ) From 9b20b703dc03e824450f119d455954cb323e9dc2 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Thu, 21 Nov 2024 15:04:08 +0100 Subject: [PATCH 011/103] cleanup --- __init__.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/__init__.py b/__init__.py index 0943139..935fc2b 100644 --- a/__init__.py +++ b/__init__.py @@ -311,7 +311,6 @@ class ABC3D_UL_texts(bpy.types.UIList): def invoke(self, context, event): pass - class ABC3D_PT_Panel(bpy.types.Panel): bl_label = f"{__name__} panel" bl_category = "ABC3D" @@ -333,27 +332,6 @@ class ABC3D_PT_Panel(bpy.types.Panel): text="open asset directory", icon='FILEBROWSER') -# class ABC3D_PT_LoadFontPanel(bpy.types.Panel): - # bl_label = "Install a new font" - # bl_parent_id = "ABC3D_PT_Panel" - # bl_category = "ABC3D" - # bl_space_type = "VIEW_3D" - # bl_region_type = "UI" - - # def draw(self, context): - # layout = self.layout - # wm = context.window_manager - # scene = context.scene - - # abc3d_data = scene.abc3d_data - - # box = layout.box() - # box.row().label(text="1. Select fontfile") - # box.row().prop(context.scene.abc3d_data, "font_path") - # box.row().label(text="2. Install it:") - # box.row().operator(f"{__name__}.install_font", text='Install new font') - - class ABC3D_PT_FontList(bpy.types.Panel): bl_label = "Font List" bl_parent_id = "ABC3D_PT_Panel" @@ -1449,7 +1427,6 @@ classes = ( ABC3D_UL_fonts, ABC3D_UL_texts, ABC3D_PT_Panel, - # ABC3D_PT_LoadFontPanel, ABC3D_PT_FontList, ABC3D_PT_TextPlacement, ABC3D_PT_TextManagement, From b9bd72f97939a4b2785f1c4a48a0d2caf0179b54 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sat, 7 Dec 2024 14:28:37 +0100 Subject: [PATCH 012/103] remove outdated comments --- common/Font.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/common/Font.py b/common/Font.py index ef34499..aa936db 100644 --- a/common/Font.py +++ b/common/Font.py @@ -120,9 +120,6 @@ class Font: self.faces = faces -# 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({}) From 20fb69465bb37ac7b070710ed52247ae61b88e91 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sat, 7 Dec 2024 14:57:33 +0100 Subject: [PATCH 013/103] manual positioning closes #1 --- __init__.py | 35 +++++--- addon_updater.py | 1 - butils.py | 204 +++++++++++++++++++++++++++++++++-------------- 3 files changed, 168 insertions(+), 72 deletions(-) diff --git a/__init__.py b/__init__.py index 935fc2b..5e40091 100644 --- a/__init__.py +++ b/__init__.py @@ -383,10 +383,10 @@ class ABC3D_PT_FontList(bpy.types.Panel): row.scale_y = scale_y row.label(text=text) row = layout.row() - oper = row.operator(f"{__name__}.load_font", + oper_lf = row.operator(f"{__name__}.load_font", text='Load all glyphs in memory') - oper.font_name = font_name - oper.face_name = face_name + oper_lf.font_name = font_name + oper_lf.face_name = face_name class ABC3D_PT_TextPlacement(bpy.types.Panel): @@ -914,6 +914,7 @@ class ABC3D_OT_RemoveText(bpy.types.Operator): def delif(o, p): if p in o: del o[p] + delif(mom, f"{utils.prefix()}_type") delif(mom, f"{utils.prefix()}_linked_textobject") delif(mom, f"{utils.prefix()}_font_name") delif(mom, f"{utils.prefix()}_face_name") @@ -1046,7 +1047,7 @@ class ABC3D_OT_PlaceText(bpy.types.Operator): # t.text) # or this: # butils.set_text_on_curve(t) - # else: + else: butils.ShowMessageBox( title="No object selected", message=( @@ -1372,7 +1373,7 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): font_name, face_name, glyph_id, - o) + bpy.types.PointerProperty(o)) # TODO: is there a better way to iterate over a CollectionProperty? found = False @@ -1484,7 +1485,6 @@ def detect_text(): def load_used_glyphs(): - print("LOAD USED GLYPHS") scene = bpy.context.scene abc3d_data = scene.abc3d_data for t in abc3d_data.available_texts: @@ -1528,19 +1528,33 @@ def on_frame_changed(self, dummy): # TODO PERFORMANCE: only on demand butils.set_text_on_curve(t) +depsgraph_updates_locked = False +def unlock_depsgraph_updates(): + global depsgraph_updates_locked + depsgraph_updates_locked = False + +def lock_depsgraph_updates(): + global depsgraph_updates_locked + depsgraph_updates_locked = True + if bpy.app.timers.is_registered(unlock_depsgraph_updates): + bpy.app.timers.unregister(unlock_depsgraph_updates) + bpy.app.timers.register(unlock_depsgraph_updates, first_interval=1) + +import time @persistent def on_depsgraph_update(scene, depsgraph): - if not bpy.context.mode.startswith("EDIT"): + global depsgraph_updates_locked + if not bpy.context.mode.startswith("EDIT") and not depsgraph_updates_locked: for u in depsgraph.updates: if f"{utils.prefix()}_linked_textobject" in u.id.keys() \ and f"{utils.prefix()}_type" in u.id.keys() \ and u.id[f"{utils.prefix()}_type"] == 'textobject': linked_textobject = u.id[f"{utils.prefix()}_linked_textobject"] if u.is_updated_geometry and len(scene.abc3d_data.available_texts) > linked_textobject: + lock_depsgraph_updates() def later(): if not "lock_depsgraph_update_ntimes" in scene.abc3d_data \ - or scene.abc3d_data["lock_depsgraph_update_ntimes"] == 0: - print("******* not yet") + or scene.abc3d_data["lock_depsgraph_update_ntimes"] <= 0: butils.set_text_on_curve( scene.abc3d_data.available_texts[linked_textobject]) elif scene.abc3d_data["lock_depsgraph_update_ntimes"] > 0: @@ -1596,6 +1610,9 @@ def unregister(): if on_frame_changed in bpy.app.handlers.frame_change_post: bpy.app.handlers.frame_change_post.remove(on_frame_changed) + if on_depsgraph_update in bpy.app.handlers.depsgraph_update_post: + bpy.app.handlers.depsgraph_update_post.remove(on_depsgraph_update) + del bpy.types.Scene.abc3d_data print(f"UNREGISTER {bl_info['name']}") diff --git a/addon_updater.py b/addon_updater.py index ca9e6d1..3ca5a3e 100644 --- a/addon_updater.py +++ b/addon_updater.py @@ -1782,7 +1782,6 @@ class ForgejoEngine: "zipball_url": self.get_zip_url(tag["commit"]["sha"], updater) } for tag in response] - # ----------------------------------------------------------------------------- # The module-shared class instance, # should be what's imported to other files diff --git a/butils.py b/butils.py index 00e5b14..b3b5e00 100644 --- a/butils.py +++ b/butils.py @@ -1,11 +1,12 @@ -import bpy -import mathutils -import queue import importlib import os +import queue import re from multiprocessing import Process +import bpy +import mathutils + # import time # for debugging performance # then import dependencies for our addon @@ -111,7 +112,8 @@ def calc_tangent_on_bezier(bezier_point_1, bezier_point_2, t): (-3 * (t**2) + 6 * t * (1 - t)) * h2 + (3 * t**2) * p2 ).normalized() -from math import radians, sqrt, pi, acos +from math import acos, pi, radians, sqrt + def align_rotations_auto_pivot(mask, input_rotations, vectors, factors, local_main_axis): output_rotations = [mathutils.Matrix().to_3x3() for _ in range(len(input_rotations))] @@ -449,6 +451,7 @@ def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""): glyph_obj = move_in_fontcollection( o, fontcollection) + glyph_obj_pointer = bpy.types.PointerProperty(glyph_obj) if glyph_obj == o: del o[marker_property] @@ -457,7 +460,7 @@ def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""): font_name, face_name, glyph_id, - glyph_obj) + glyph_obj_pointer) for c in o.children: if is_metrics_object(c): add_metrics_obj_from_bound_box(glyph_obj, @@ -472,7 +475,7 @@ def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""): for g in face.glyphs: # iterate alternates for glyph in face.glyphs[g]: - glyphs.append(glyph) + glyphs.append(get_original(glyph)) if len(glyphs) > 0: add_default_metrics_to_objects(glyphs) # calculate unit factor @@ -586,18 +589,19 @@ def ShowMessageBox(title = "Message Box", icon = 'INFO', message=""): bpy.context.window_manager.popup_menu(draw, title = title, icon = icon) 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() + completely_delete_objects(objs) -def completely_delete_objects(objs): - simply_delete_objects(objs) - - # remove deleted objects - # this is necessary +def completely_delete_objects(objs, recursive=True): for g in objs: if type(g) != type(None): + if recursive: + try: + if hasattr(g, "children") and len(g.children) > 0: + completely_delete_objects(g.children) + except ReferenceError as e: + # not important + pass + try: bpy.data.objects.remove(g, do_unlink=True) except ReferenceError as e: @@ -672,51 +676,80 @@ def prepare_text(font_name, face_name, text): return True def is_bezier(curve): + if curve.type != 'CURVE': + return False if len(curve.data.splines) < 1: return False - return curve.data.splines[0].type == 'BEZIER' + for spline in curve.data.splines: + if spline.type != 'BEZIER': + return False + return True + +def will_regenerate(text_properties): + mom = text_properties.text_object + + if len(text_properties.text) != len(text_properties.glyphs): + return True + + for i, g in enumerate(text_properties.glyphs): + if not hasattr(g.glyph_object, "type"): + return True + elif g.glyph_object.type != 'EMPTY': + return True + # check if perhaps one glyph was deleted + elif type(g.glyph_object) == type(None): + return True + elif type(g.glyph_object.parent) == type(None): + return True + elif g.glyph_object.parent.users_collection != g.glyph_object.users_collection: + return True + elif len(text_properties.text) > i and g.glyph_id != text_properties.text[i]: + return True + elif len(text_properties.text) > i and (g.glyph_object[f"{utils.prefix()}_font_name"] != text_properties.font_name + or g.glyph_object[f"{utils.prefix()}_face_name"] != text_properties.face_name): + return True + + return False + + +def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4): + """set_text_on_curve + + An earlier reset cancels the other. + To disable reset, set both to false. + + :param text_properties: all information necessary to set text on a curve + :type text_properties: ABC3D_text_properties + :param reset_timeout_s: reset external parameters after timeout. (<= 0) = immediate, (> 0) = non-blocking reset timeout in seconds, (False) = no timeout reset + :type reset_timeout_s: float + :param reset_depsgraph_n: reset external parameters after n-th depsgraph update. (<= 0) = immediate, (> 0) = reset after n-th depsgraph update, (False) = no depsgraph reset + :type reset_depsgraph_n: int + """ -def set_text_on_curve(text_properties, recursive=True): # starttime = time.perf_counter_ns() mom = text_properties.text_object if mom.type != "CURVE": return False - regenerate = False - glyph_objects = [] - for i, g in enumerate(text_properties.glyphs): - glyph_objects.append(g.glyph_object) + distribution_type = 'CALCULATE' if is_bezier(mom) else 'FOLLOW_PATH' - # check if perhaps one glyph was deleted - if (type(g.glyph_object) == type(None) - or type(g.glyph_object.parent) == type(None) - or g.glyph_object.parent.users_collection != g.glyph_object.users_collection): - regenerate = True - elif len(text_properties.text) > i and g.glyph_id != text_properties.text[i]: - regenerate = True - elif len(text_properties.text) > i and (g.glyph_object[f"{utils.prefix()}_font_name"] != text_properties.font_name - or g.glyph_object[f"{utils.prefix()}_face_name"] != text_properties.face_name): - regenerate = True - - if len(text_properties.text) != len(text_properties.glyphs): - regenerate = True - - # blender bug + # use_path messes with parenting + # however, we need it for follow_path # https://projects.blender.org/blender/blender/issues/100661 - if mom.data.use_path: - regenerate = True + previous_use_path = mom.data.use_path + if distribution_type == 'CALCULATE': + mom.data.use_path = False + elif distribution_type == 'FOLLOW_PATH': + mom.data.use_path = True + + regenerate = will_regenerate(text_properties) # if we regenerate.... delete objects - if regenerate: - completely_delete_objects(glyph_objects) - + if regenerate and text_properties.get("glyphs"): + glyph_objects = [ g["glyph_object"] for g in text_properties["glyphs"] ] + completely_delete_objects(glyph_objects, True) text_properties.glyphs.clear() -#TODO: fix selection with context_override - previous_selection = bpy.context.selected_objects if hasattr(bpy.context, "selected_objects") else [ o for o in bpy.context.scene.objects if o.select_get() ] - bpy.ops.object.select_all(action='DESELECT') - selected_objects = [] - curve_length = get_curve_length(mom) advance = text_properties.offset glyph_advance = 0 @@ -747,7 +780,7 @@ def set_text_on_curve(text_properties, recursive=True): glyph = Font.get_glyph(text_properties.font_name, text_properties.face_name, - glyph_id) + glyph_id).original if glyph == None: # self.report({'ERROR'}, f"Glyph not found for {font_name} {face_name} {glyph_id}") @@ -755,16 +788,20 @@ def set_text_on_curve(text_properties, recursive=True): continue ob = None + obg = None if regenerate: - ob = bpy.data.objects.new(f"{glyph_id}", glyph.data) + ob = bpy.data.objects.new(f"{glyph_id}", None) + obg = bpy.data.objects.new(f"{glyph_id}_mesh", glyph.data) ob[f"{utils.prefix()}_type"] = "glyph" ob[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id ob[f"{utils.prefix()}_font_name"] = text_properties.font_name ob[f"{utils.prefix()}_face_name"] = text_properties.face_name else: ob = text_properties.glyphs[i].glyph_object + for c in ob.children: + if c.name.startswith(f"{glyph_id}_mesh"): + obg = c - distribution_type = 'CALCULATE' if is_bezier(mom) else 'FOLLOW_PATH' if distribution_type == 'FOLLOW_PATH': ob.constraints.new(type='FOLLOW_PATH') ob.constraints["Follow Path"].target = mom @@ -773,6 +810,7 @@ def set_text_on_curve(text_properties, recursive=True): ob.constraints["Follow Path"].use_curve_follow = True ob.constraints["Follow Path"].forward_axis = "FORWARD_X" ob.constraints["Follow Path"].up_axis = "UP_Y" + spline_index = 0 elif distribution_type == 'CALCULATE': location, tangent, spline_index = calc_point_on_bezier_curve(mom, advance, True, True) if spline_index != previous_spline_index: @@ -780,6 +818,11 @@ def set_text_on_curve(text_properties, recursive=True): if regenerate: ob.location = mom.matrix_world @ (location + text_properties.translation) + mom.users_collection[0].objects.link(obg) + mom.users_collection[0].objects.link(ob) + ob.parent = mom + obg.parent = ob + obg.location = mathutils.Vector((0.0, 0.0, 0.0)) else: ob.location = (location + text_properties.translation) @@ -796,16 +839,20 @@ def set_text_on_curve(text_properties, recursive=True): local_main_axis) if ob.rotation_mode != 'QUATERNION': ob.rotation_mode = 'QUATERNION' + if obg.rotation_mode != 'QUATERNION': + obg.rotation_mode = 'QUATERNION' q = mathutils.Quaternion() q.rotate(text_properties.orientation) if regenerate: - ob.rotation_quaternion = (mom.matrix_world @ motor[0] @ q.to_matrix().to_4x4()).to_quaternion() + obg.rotation_quaternion = q + ob.rotation_quaternion = (mom.matrix_world @ motor[0]).to_quaternion() else: - ob.rotation_quaternion = (motor[0] @ q.to_matrix().to_4x4()).to_quaternion() + ob.rotation_quaternion = motor[0].to_quaternion() else: q = mathutils.Quaternion() q.rotate(text_properties.orientation) - ob.rotation_quaternion = q + # obg.rotation_quaternion = q + obg.rotation_quaternion = (mom.matrix_world @ q.to_matrix().to_4x4()).to_quaternion() # ob.rotation_quaternion = (mom.matrix_world @ q.to_matrix().to_4x4()).to_quaternion() @@ -839,17 +886,12 @@ def set_text_on_curve(text_properties, recursive=True): previous_spline_index = spline_index if regenerate: - mom.users_collection[0].objects.link(ob) glyph_data = text_properties.glyphs.add() glyph_data.glyph_id = glyph_id glyph_data.glyph_object = ob glyph_data.letter_spacing = 0 - ob.select_set(True) - + if regenerate: - mom.select_set(True) - # https://projects.blender.org/blender/blender/issues/100661 - mom.data.use_path = False mom[f"{utils.prefix()}_type"] = "textobject" mom[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id mom[f"{utils.prefix()}_font_name"] = text_properties.font_name @@ -858,10 +900,42 @@ def set_text_on_curve(text_properties, recursive=True): mom[f"{utils.prefix()}_letter_spacing"] = text_properties.letter_spacing mom[f"{utils.prefix()}_orientation"] = text_properties.orientation mom[f"{utils.prefix()}_translation"] = text_properties.translation - bpy.context.view_layer.objects.active = mom - bpy.ops.object.parent_set(type='OBJECT') - bpy.context.scene.abc3d_data["lock_depsgraph_update_ntimes"] = len(bpy.context.selected_objects) - mom["lock_depsgraph_update_ntimes"] = len(bpy.context.selected_objects) + + if "lock_depsgraph_update_ntimes" in bpy.context.scene.abc3d_data: + bpy.context.scene.abc3d_data["lock_depsgraph_update_ntimes"] += len(bpy.context.selected_objects) + else: + bpy.context.scene.abc3d_data["lock_depsgraph_update_ntimes"] = len(bpy.context.selected_objects) + + # NOTE: we reset with a timeout, as setting and resetting certain things + # in fast succession will cause visual glitches (e.g. {}.data.use_path). + def reset(): + mom.data.use_path = previous_use_path + if counted_reset in bpy.app.handlers.depsgraph_update_post: + bpy.app.handlers.depsgraph_update_post.remove(counted_reset) + if bpy.app.timers.is_registered(reset): + bpy.app.timers.unregister(reset) + + molotov = reset_depsgraph_n + 0 + def counted_reset(scene, depsgraph): + nonlocal molotov + if molotov == 0: + reset() + else: + molotov -= 1 + + # unregister previous resets to avoid multiple execution + if bpy.app.timers.is_registered(reset): + bpy.app.timers.unregister(reset) + if counted_reset in bpy.app.handlers.depsgraph_update_post: + bpy.app.handlers.depsgraph_update_post.remove(counted_reset) + + if not isinstance(reset_timeout_s, bool): + if reset_timeout_s > 0: + bpy.app.timers.register(reset, first_interval=reset_timeout_s) + elif reset_timeout <= 0: + reset() + + bpy.app.handlers.depsgraph_update_post.append(counted_reset) # endtime = time.perf_counter_ns() # elapsedtime = endtime - starttime @@ -1095,6 +1169,12 @@ def get_metrics_object(o): return c return None +def get_original(o): + if hasattr(o, "original"): + return o.original + else: + return o + def add_default_metrics_to_objects(objects=None, overwrite_existing=False): if type(objects) == type(None): objects=bpy.context.selected_objects From 648d4a6dee0b938be8859a1aed65784d534110ca Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sat, 7 Dec 2024 15:07:11 +0100 Subject: [PATCH 014/103] bump version v0.0.3 --- __init__.py | 3 ++- common/utils.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/__init__.py b/__init__.py index 5e40091..3fe519a 100644 --- a/__init__.py +++ b/__init__.py @@ -15,12 +15,13 @@ import importlib bl_info = { "name": "ABC3D", "author": "Jakob Schlötter, Studio Pointer*", - "version": (0, 0, 2), + "version": (0, 0, 3), "blender": (4, 1, 0), "location": "VIEW3D", "description": "Convenience addon for 3D fonts", "category": "Typography", } +# NOTE: also change version in common/utils.py # make sure that modules are reloadable # when registering diff --git a/common/utils.py b/common/utils.py index e9757b4..1e815e8 100644 --- a/common/utils.py +++ b/common/utils.py @@ -1,10 +1,10 @@ - +# NOTE: also change version in ../__init__.py def get_version_major(): return 0 def get_version_minor(): return 0 def get_version_patch(): - return 2 + return 3 def get_version_string(): return f"{get_version_major()}.{get_version_minor()}.{get_version_patch}" def prefix(): From cddbc7915165d712bf0ce4b2341ef8c64c75842d Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sat, 18 Jan 2025 17:18:23 +0100 Subject: [PATCH 015/103] add requirements.txt useful for development --- requirements.txt | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..fcb9ea9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,33 @@ +astroid==3.3.5 +attrs==24.2.0 +black==24.10.0 +bpy==4.2.0 +cattrs==24.1.2 +certifi==2024.8.30 +charset-normalizer==3.4.0 +click==8.1.7 +Cython==3.0.11 +dill==0.3.9 +docstring-to-markdown==0.15 +flake8==7.1.1 +idna==3.10 +isort==5.13.2 +jedi==0.19.1 +jedi-language-server==0.41.4 +lsprotocol==2023.0.1 +mathutils==3.3.0 +mccabe==0.7.0 +mypy-extensions==1.0.0 +numpy==2.1.3 +packaging==24.1 +parso==0.8.4 +pathspec==0.12.1 +platformdirs==4.3.6 +pycodestyle==2.12.1 +pyflakes==3.2.0 +pygls==1.3.1 +pylint==3.3.1 +requests==2.32.3 +tomlkit==0.13.2 +urllib3==2.2.3 +zstandard==0.23.0 From e69cdc951d00e6d730b047027f8a4accd0bba1c9 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sat, 18 Jan 2025 18:19:52 +0100 Subject: [PATCH 016/103] cosmetics --- __init__.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/__init__.py b/__init__.py index 3fe519a..72193c4 100644 --- a/__init__.py +++ b/__init__.py @@ -242,9 +242,6 @@ class ABC3D_text_properties(bpy.types.PropertyGroup): distribution_type: bpy.props.StringProperty() glyphs: bpy.props.CollectionProperty(type=ABC3D_glyph_properties) -# TODO: simply, merge, cut cut cut - - class ABC3D_data(bpy.types.PropertyGroup): available_fonts: bpy.props.CollectionProperty( type=ABC3D_available_font, name="Available fonts") @@ -1563,8 +1560,8 @@ def on_depsgraph_update(scene, depsgraph): butils.run_in_main_thread(later) - def register(): + print(f"REGISTER {utils.prefix()}") addon_updater_ops.register(bl_info) for cls in classes: @@ -1572,9 +1569,8 @@ def register(): bpy.utils.register_class(cls) bpy.types.Scene.abc3d_data = bpy.props.PointerProperty(type=ABC3D_data) # bpy.types.Object.__del__ = lambda self: print(f"Bye {self.name}") - print(f"REGISTER {bl_info['name']}") - # auto start if we load a blend file + # autostart if we load a blend file if load_handler not in bpy.app.handlers.load_post: bpy.app.handlers.load_post.append(load_handler) # and autostart if we reload script @@ -1615,7 +1611,7 @@ def unregister(): bpy.app.handlers.depsgraph_update_post.remove(on_depsgraph_update) del bpy.types.Scene.abc3d_data - print(f"UNREGISTER {bl_info['name']}") + print(f"UNREGISTER {utils.prefix()}") if __name__ == '__main__': From 1fbac99bd88d40c83615f70387ffd7f8bd03a3df Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sat, 18 Jan 2025 18:21:33 +0100 Subject: [PATCH 017/103] add space recognition --- __init__.py | 2 +- common/Font.py | 42 +++++++++++++++++++++++++++++++++++----- common/spacesUnicode.txt | 23 ++++++++++++++++++++++ 3 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 common/spacesUnicode.txt diff --git a/__init__.py b/__init__.py index 72193c4..cb3c10d 100644 --- a/__init__.py +++ b/__init__.py @@ -1589,7 +1589,7 @@ def register(): # bpy.ops.abc3d.load_installed_fonts() - Font.name_to_glyph_d = Font.generate_name_to_glyph_d() + Font.init() def unregister(): diff --git a/common/Font.py b/common/Font.py index aa936db..7b36da5 100644 --- a/common/Font.py +++ b/common/Font.py @@ -37,6 +37,8 @@ name_to_glyph_d = { "space": " ", } +space_d = {} + known_misspellings = { # simple misspelling "excent" : "accent", @@ -74,17 +76,47 @@ def name_to_glyph(name): else: return None -def generate_name_to_glyph_d(): + +def is_space(character): + for name in space_d: + if character == space_d[name][0]: + return space_d[name][1] + return False + + +def generate_from_file_d(filepath): + print(f"{filepath=}") d = {} - with open(f"{Path(__file__).parent}/glyphNamesToUnicode.txt") as f: + with open(filepath) as f: for line in f: if line[0] == '#': continue - (name, hexstr) = line.split(' ') - val = chr(int(hexstr, base=16)) - d[name] = val + split = line.split(' ') + if len(split) == 2: + (name, hexstr) = line.split(' ') + val = chr(int(hexstr, base=16)) + d[name] = val + print(f"{name=} {val=}") + if len(split) == 3: + # we might have a parameter, like for the spaces + (name, hexstr, parameter) = line.split(' ') + parameter_value = float(parameter) + val = chr(int(hexstr, base=16)) + d[name] = [val, parameter_value] + print(f"{name=} {val=}, {parameter_value=}, {parameter_value * 2}") return d +def generate_name_to_glyph_d(): + return generate_from_file_d(f"{Path(__file__).parent}/glyphNamesToUnicode.txt") + +def generate_space_d(): + return generate_from_file_d(f"{Path(__file__).parent}/spacesUnicode.txt") + +def init(): + global name_to_glyph_d + global space_d + name_to_glyph_d = generate_name_to_glyph_d() + space_d = generate_space_d() class FontFace: """FontFace is a class holding glyphs diff --git a/common/spacesUnicode.txt b/common/spacesUnicode.txt new file mode 100644 index 0000000..da6a7c9 --- /dev/null +++ b/common/spacesUnicode.txt @@ -0,0 +1,23 @@ +# The space value derives from The Elements of Typographic Style +# same for en-/em values. Rest are rough guesses. +space 0020 0.25 +nbspace 00A0 0.25 +# ethi:wordspace 1361 # NOTE: has shape +enquad 2000 0.5 +emquad 2001 1 +enspace 2002 0.5 +emspace 2003 1 +threeperemspace 2004 3 +fourperemspace 2005 4 +sixperemspace 2006 6 +figurespace 2007 1 +punctuationspace 2008 1 +thinspace 2009 0.1 +hairspace 200A 0.05 +zerowidthspace 200B 0 +narrownobreakspace 202F 0.1 +mediummathematicalspace 205F 1 +cntr:space 2420 0.25 +ideographicspace 3000 1 +# ideographichalffillspace 303F # NOTE: has shape +zerowidthnobreakspace FEFF 0 From d13afa7d7d7ffdf5eeb9d3366800f7f32254675c Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sat, 18 Jan 2025 18:22:32 +0100 Subject: [PATCH 018/103] friendliness prevent repetitive messages --- butils.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/butils.py b/butils.py index b3b5e00..1d49582 100644 --- a/butils.py +++ b/butils.py @@ -553,7 +553,9 @@ def register_installed_fonts(): # print(f"available font: {f.font_name} {f.face_name}") register_font_from_filepath(font_path) -def ShowMessageBox(title = "Message Box", icon = 'INFO', message=""): +message_memory = [] + +def ShowMessageBox(title = "Message Box", icon = 'INFO', message="", prevent_repeat=False): """Show a simple message box @@ -579,6 +581,13 @@ def ShowMessageBox(title = "Message Box", icon = 'INFO', message=""): butils.ShowMessageBox(title="",message=("AAAAAH","NOOOOO"),icon=) """ + global message_memory + if prevent_repeat: + for m in message_memory: + if m[0] == title and m[1] == icon and m[2] == message: + print("PREVENT PREVENT") + return + message_memory.append([title, icon, message]) myLines=message def draw(self, context): if isinstance(myLines, str): From 36c8f25e29f80ddde5e6fdad755a9ecae590402e Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sun, 19 Jan 2025 12:03:42 +0100 Subject: [PATCH 019/103] reset rotation mode --- butils.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/butils.py b/butils.py index 1d49582..e29358d 100644 --- a/butils.py +++ b/butils.py @@ -821,6 +821,15 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) ob.constraints["Follow Path"].up_axis = "UP_Y" spline_index = 0 elif distribution_type == 'CALCULATE': + previous_ob_rotation_mode = None + previous_obg_rotation_mode = None + if ob.rotation_mode != 'QUATERNION': + ob.rotation_mode = 'QUATERNION' + previous_ob_rotation_mode = ob.rotation_mode + if obg.rotation_mode != 'QUATERNION': + obg.rotation_mode = 'QUATERNION' + previous_obg_rotation_mode = obg.rotation_mode + location, tangent, spline_index = calc_point_on_bezier_curve(mom, advance, True, True) if spline_index != previous_spline_index: is_newline = True @@ -846,10 +855,7 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) vectors, factors, local_main_axis) - if ob.rotation_mode != 'QUATERNION': - ob.rotation_mode = 'QUATERNION' - if obg.rotation_mode != 'QUATERNION': - obg.rotation_mode = 'QUATERNION' + q = mathutils.Quaternion() q.rotate(text_properties.orientation) if regenerate: @@ -864,6 +870,10 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) obg.rotation_quaternion = (mom.matrix_world @ q.to_matrix().to_4x4()).to_quaternion() # ob.rotation_quaternion = (mom.matrix_world @ q.to_matrix().to_4x4()).to_quaternion() + if previous_ob_rotation_mode: + ob.rotation_mode = previous_ob_rotation_mode + if previous_obg_rotation_mode: + obg.rotation_mode = previous_obg_rotation_mode glyph_advance = get_glyph_advance(glyph) * scalor + text_properties.letter_spacing From 167dea8164ff2cd26aa62055d330ca57ea62fe81 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sun, 19 Jan 2025 14:19:59 +0100 Subject: [PATCH 020/103] allow replacements (upper/lower) --- butils.py | 48 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/butils.py b/butils.py index e29358d..81bcf41 100644 --- a/butils.py +++ b/butils.py @@ -674,11 +674,23 @@ def get_glyph_height(glyph_obj): 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): +def prepare_text(font_name, face_name, text, allow_replacement=True): loaded, missing, loadable, files = Font.test_glyphs_availability( font_name, face_name, text) + # possibly replace upper and lower case letters with each other + if len(missing) > 0 and allow_replacement: + replacement_search = "" + for m in missing: + if m.islower(): + replacement_search += m.upper() + if m.isupper(): + replacement_search += m.lower() + r = Font.test_availability(font_name, face_name, replacement_search) + loadable += r["maybe"] + # not update (loaded, missing, files), we only use loadable/maybe later + if len(loadable) > 0: for filepath in files: load_font_from_filepath(filepath, loadable, font_name, face_name) @@ -770,9 +782,6 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) if c == '\\': is_command = True continue - if c == ' ': - advance = advance + scalor - continue is_newline = False if is_command: if c == 'n': @@ -787,14 +796,33 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) is_command = False glyph_id = c - glyph = Font.get_glyph(text_properties.font_name, + glyph_tmp = Font.get_glyph(text_properties.font_name, text_properties.face_name, - glyph_id).original + glyph_id) + if glyph_tmp == None: + space_width = Font.is_space(glyph_id) + if space_width != False: + advance = advance + space_width * text_properties.font_size + continue - if glyph == None: - # self.report({'ERROR'}, f"Glyph not found for {font_name} {face_name} {glyph_id}") - print(f"Glyph not found for {text_properties.font_name} {text_properties.face_name} {glyph_id}") - continue + message=f"Glyph not found for font_name='{text_properties.font_name}' face_name='{text_properties.face_name}' glyph_id='{glyph_id}'" + replaced = False + if glyph_id.isupper() or glyph_id.islower(): + possible_replacement = glyph_id.lower() if glyph_id.isupper() else glyph_id.upper() + glyph_tmp = Font.get_glyph(text_properties.font_name, + text_properties.face_name, + possible_replacement) + if glyph_tmp != None: + message = message + f" (replaced with '{possible_replacement}')" + replaced = True + + ShowMessageBox(title="Glyph replaced" if replaced else "Glyph missing", + icon='INFO' if replaced else 'ERROR', + message=message, + prevent_repeat=True) + if replaced == False: + continue + glyph = glyph_tmp.original ob = None obg = None From f046546e61de6556aa1469315c316a0e70c8120c Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sun, 19 Jan 2025 14:20:14 +0100 Subject: [PATCH 021/103] less print --- common/Font.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/common/Font.py b/common/Font.py index 7b36da5..1c8368e 100644 --- a/common/Font.py +++ b/common/Font.py @@ -85,7 +85,6 @@ def is_space(character): def generate_from_file_d(filepath): - print(f"{filepath=}") d = {} with open(filepath) as f: for line in f: @@ -96,14 +95,12 @@ def generate_from_file_d(filepath): (name, hexstr) = line.split(' ') val = chr(int(hexstr, base=16)) d[name] = val - print(f"{name=} {val=}") if len(split) == 3: # we might have a parameter, like for the spaces (name, hexstr, parameter) = line.split(' ') parameter_value = float(parameter) val = chr(int(hexstr, base=16)) d[name] = [val, parameter_value] - print(f"{name=} {val=}, {parameter_value=}, {parameter_value * 2}") return d def generate_name_to_glyph_d(): @@ -206,19 +203,19 @@ def get_glyph(font_name, face_name, glyph_id, alternate=0): """ if not fonts.keys().__contains__(font_name): - print(f"ABC3D::get_glyph: font name({font_name}) not found") - print(fonts.keys()) + # print(f"ABC3D::get_glyph: font name({font_name}) not found") + # print(fonts.keys()) return None face = fonts[font_name].faces.get(face_name) if face == None: - print(f"ABC3D::get_glyph: font({font_name}) face({face_name}) not found") - print(fonts[font_name].faces.keys()) + # print(f"ABC3D::get_glyph: font({font_name}) face({face_name}) not found") + # print(fonts[font_name].faces.keys()) return None 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") + # 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 From 2f94702ea9b4e32ce250e19c2b3e687c4e0f9786 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sun, 19 Jan 2025 14:20:37 +0100 Subject: [PATCH 022/103] add removeNonAlphabetic --- common/utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/common/utils.py b/common/utils.py index 1e815e8..63c4690 100644 --- a/common/utils.py +++ b/common/utils.py @@ -72,6 +72,10 @@ def printerr(*args, **kwargs): print(*args, file=sys.stderr, **kwargs) +def removeNonAlphabetic(s): + return ''.join([i for i in s if i.isalpha()]) + + # # Evaluate a bezier curve for the parameter 0<=t<=1 along its length # def evaluateBezierPoint(p1, h1, h2, p2, t): # return ((1 - t)**3) * p1 + (3 * t * (1 - t)**2) * h1 + (3 * (t**2) * (1 - t)) * h2 + (t**3) * p2 From 490723496c8a95bd5ecd4d79aab341b77faaa34b Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sun, 19 Jan 2025 14:21:21 +0100 Subject: [PATCH 023/103] simplify --- butils.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/butils.py b/butils.py index 81bcf41..b9488ba 100644 --- a/butils.py +++ b/butils.py @@ -683,10 +683,8 @@ def prepare_text(font_name, face_name, text, allow_replacement=True): if len(missing) > 0 and allow_replacement: replacement_search = "" for m in missing: - if m.islower(): - replacement_search += m.upper() - if m.isupper(): - replacement_search += m.lower() + if m.isalpha(): + replacement_search += m.swapcase() r = Font.test_availability(font_name, face_name, replacement_search) loadable += r["maybe"] # not update (loaded, missing, files), we only use loadable/maybe later @@ -807,8 +805,8 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) message=f"Glyph not found for font_name='{text_properties.font_name}' face_name='{text_properties.face_name}' glyph_id='{glyph_id}'" replaced = False - if glyph_id.isupper() or glyph_id.islower(): - possible_replacement = glyph_id.lower() if glyph_id.isupper() else glyph_id.upper() + if glyph_id.isalpha(): + possible_replacement = glyph_id.swapcase() glyph_tmp = Font.get_glyph(text_properties.font_name, text_properties.face_name, possible_replacement) From b40d49c723fcca9a3c1dcd97e1e6d40efc5d5225 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sun, 19 Jan 2025 14:24:23 +0100 Subject: [PATCH 024/103] bump version v0.0.4 --- README.md | 1 + __init__.py | 2 +- common/utils.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9b5d420..9e500d6 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ / ___ \| |_) | |___ ___) | |_| | /_/ \_\____/ \____|____/|____/ ``` +v0.0.4 Convenience tool to work with 3D typography in Blender and Cinema4D. diff --git a/__init__.py b/__init__.py index cb3c10d..b3ca721 100644 --- a/__init__.py +++ b/__init__.py @@ -15,7 +15,7 @@ import importlib bl_info = { "name": "ABC3D", "author": "Jakob Schlötter, Studio Pointer*", - "version": (0, 0, 3), + "version": (0, 0, 4), "blender": (4, 1, 0), "location": "VIEW3D", "description": "Convenience addon for 3D fonts", diff --git a/common/utils.py b/common/utils.py index 63c4690..5a23e78 100644 --- a/common/utils.py +++ b/common/utils.py @@ -4,7 +4,7 @@ def get_version_major(): def get_version_minor(): return 0 def get_version_patch(): - return 3 + return 4 def get_version_string(): return f"{get_version_major()}.{get_version_minor()}.{get_version_patch}" def prefix(): From 92529854055014fc714d303326a2e7b3c189f06a Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Tue, 13 May 2025 15:11:07 +0200 Subject: [PATCH 025/103] version bump --- __init__.py | 2 +- common/utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/__init__.py b/__init__.py index b3ca721..94aa3f2 100644 --- a/__init__.py +++ b/__init__.py @@ -15,7 +15,7 @@ import importlib bl_info = { "name": "ABC3D", "author": "Jakob Schlötter, Studio Pointer*", - "version": (0, 0, 4), + "version": (0, 0, 5), "blender": (4, 1, 0), "location": "VIEW3D", "description": "Convenience addon for 3D fonts", diff --git a/common/utils.py b/common/utils.py index 5a23e78..d1eab92 100644 --- a/common/utils.py +++ b/common/utils.py @@ -4,7 +4,7 @@ def get_version_major(): def get_version_minor(): return 0 def get_version_patch(): - return 4 + return 5 def get_version_string(): return f"{get_version_major()}.{get_version_minor()}.{get_version_patch}" def prefix(): From fc9b1b65b081eb8a7b20a0cf0259fa5497420dc6 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Tue, 13 May 2025 15:38:37 +0200 Subject: [PATCH 026/103] autodetect names earlier this makes it obvious to the user if autodetect works --- __init__.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/__init__.py b/__init__.py index 94aa3f2..42f4f8c 100644 --- a/__init__.py +++ b/__init__.py @@ -1248,6 +1248,18 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): wm = context.window_manager return wm.invoke_props_dialog(self) + def do_autodetect_names(self, name: str): + ifxsplit = name.split("_") + if len(ifxsplit) < 4: + print(f"name could not be autodetected {name}") + print("split:") + print(ifxsplit) + return self.font_name, self.face_name, self.import_infix + detected_font_name = f"{ifxsplit[1]}_{ifxsplit[2]}" + detected_face_name = ifxsplit[3] + detected_import_infix = f"{ifxsplit[1]}_{ifxsplit[2]}_{ifxsplit[3]}" + return detected_font_name, detected_face_name, detected_import_infix + def draw(self, context): layout = self.layout if len(context.selected_objects) == 0: @@ -1255,7 +1267,11 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): layout.row().label(text="Please select your glyphs first.", icon="INFO") else: row = layout.row() - row.prop(self, 'autodetect_names') + row.prop(self, "autodetect_names") + first_object_name = context.selected_objects[-1].name + self.font_name, self.face_name, self.import_infix = ( + self.do_autodetect_names(first_object_name) + ) if self.autodetect_names: scale_y = 0.5 row = layout.row() @@ -1342,13 +1358,7 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): print(f"processing {o.name}") process_object = True if self.autodetect_names: - ifxsplit = o.name.split('_') - if len(ifxsplit) < 4: - print( - f"whoops name could not be autodetected {o.name}") - continue - font_name = f"{ifxsplit[1]}_{ifxsplit[2]}" - face_name = ifxsplit[3] + font_name, face_name, input_infix = self.do_autodetect_names(o.name) if butils.is_mesh(o) and not butils.is_metrics_object(o): uc = o.users_collection From 21480ee37122bc7d906fd3e51e97e093f5e86381 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Tue, 13 May 2025 15:45:40 +0200 Subject: [PATCH 027/103] remove import_infix not necessary and potentially confusing --- __init__.py | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/__init__.py b/__init__.py index 42f4f8c..55cfd66 100644 --- a/__init__.py +++ b/__init__.py @@ -1234,9 +1234,6 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): face_name: bpy.props.StringProperty( default="Tender", ) - import_infix: bpy.props.StringProperty( - default="_NM_Origin_Tender", - ) autodetect_names: bpy.props.BoolProperty( default=True, ) @@ -1254,11 +1251,10 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): print(f"name could not be autodetected {name}") print("split:") print(ifxsplit) - return self.font_name, self.face_name, self.import_infix + return self.font_name, self.face_name detected_font_name = f"{ifxsplit[1]}_{ifxsplit[2]}" detected_face_name = ifxsplit[3] - detected_import_infix = f"{ifxsplit[1]}_{ifxsplit[2]}_{ifxsplit[3]}" - return detected_font_name, detected_face_name, detected_import_infix + return detected_font_name, detected_face_name def draw(self, context): layout = self.layout @@ -1269,7 +1265,7 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): row = layout.row() row.prop(self, "autodetect_names") first_object_name = context.selected_objects[-1].name - self.font_name, self.face_name, self.import_infix = ( + self.font_name, self.face_name = ( self.do_autodetect_names(first_object_name) ) if self.autodetect_names: @@ -1313,10 +1309,9 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): row.label(text="- 'colon_NM_Origin_Tender_2'") box = layout.box() box.enabled = not self.autodetect_names - box.prop(self, 'font_name') - box.prop(self, 'face_name') - box.prop(self, 'import_infix') - layout.prop(self, 'fix_common_misspellings') + box.prop(self, "font_name") + box.prop(self, "face_name") + layout.prop(self, "fix_common_misspellings") if self.fix_common_misspellings: for k in Font.known_misspellings: character = "" @@ -1341,11 +1336,6 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): if fontcollection is None: fontcollection = bpy.data.collections.new("ABC3D") - ifxsplit = self.import_infix.split('_') - # if len(ifxsplit) != 4: - - # font_name = f"{ifxsplit[1]}_{ifxsplit[2]}" - # face_name = ifxsplit[3] font_name = self.font_name face_name = self.face_name From 5c79c7e06e25477670fdb88bb84a8d1417ec7467 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Tue, 13 May 2025 15:49:45 +0200 Subject: [PATCH 028/103] reshuffle and simplify --- __init__.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/__init__.py b/__init__.py index 55cfd66..0f89da3 100644 --- a/__init__.py +++ b/__init__.py @@ -528,17 +528,12 @@ class ABC3D_PT_FontCreation(bpy.types.Panel): abc3d_data = scene.abc3d_data layout.row().operator( - f"{__name__}.create_font_from_objects", text='Create/Extend Font') - box = layout.box() - box.row().label(text="Exporting a fontfile") - box.row().label(text="1. Select export directory:") - box.prop(abc3d_data, 'export_dir') - box.row().label(text="2. More options and export:") - - box.row().operator(f"{__name__}.save_font_to_file", - text='Export Font To File') + f"{__name__}.toggle_abc3d_collection", text="Toggle Collection" + ) layout.row().operator( - f"{__name__}.toggle_abc3d_collection", text='Toggle Collection') + f"{__name__}.create_font_from_objects", text="Create/Extend Font" + ) + layout.row().operator(f"{__name__}.save_font_to_file", text="Export Font To File") box = layout.box() box.label(text="metrics") box.row().operator( From 33dac5eae1b0cc13754c113f3805234e5c21ddf7 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Tue, 13 May 2025 15:50:22 +0200 Subject: [PATCH 029/103] formating auto format made this, i like most of the changes so let's keep it --- __init__.py | 692 +++++++++++++++++++++++----------------- butils.py | 816 +++++++++++++++++++++++++++++------------------- common/utils.py | 83 +++-- 3 files changed, 941 insertions(+), 650 deletions(-) diff --git a/__init__.py b/__init__.py index 0f89da3..ad95aba 100644 --- a/__init__.py +++ b/__init__.py @@ -4,13 +4,14 @@ A 3D font helper """ +import functools +import importlib +import io import os + +import bpy from bpy.app.handlers import persistent from bpy.types import Panel -import functools -import io -import bpy -import importlib bl_info = { "name": "ABC3D", @@ -23,6 +24,9 @@ bl_info = { } # NOTE: also change version in common/utils.py +from . import addon_updater_ops, bimport, butils +from .common import Font, utils + # make sure that modules are reloadable # when registering # handy for development @@ -33,12 +37,6 @@ if "Font" in locals(): importlib.reload(butils) importlib.reload(bimport) importlib.reload(addon_updater_ops) -else: - from .common import Font - from .common import utils - from . import butils - from . import bimport - from . import addon_updater_ops def getPreferences(context): @@ -58,60 +56,68 @@ class ABC3D_addonPreferences(bpy.types.AddonPreferences): auto_check_update = bpy.props.BoolProperty( name="Auto-check for Update", description="If enabled, auto-check for updates using an interval", - default=False) + default=False, + ) updater_interval_months = bpy.props.IntProperty( - name='Months', + name="Months", description="Number of months between checking for updates", default=0, - min=0) + min=0, + ) updater_interval_days = bpy.props.IntProperty( - name='Days', + name="Days", description="Number of days between checking for updates", default=7, min=0, - max=31) + max=31, + ) updater_interval_hours = bpy.props.IntProperty( - name='Hours', + name="Hours", description="Number of hours between checking for updates", default=0, min=0, - max=23) + max=23, + ) updater_interval_minutes = bpy.props.IntProperty( - name='Minutes', + name="Minutes", description="Number of minutes between checking for updates", default=0, min=0, - max=59) + max=59, + ) def get_default_assets_dir(): - return bpy.utils.user_resource( - 'DATAFILES', - path=f"{__name__}", - create=True) + return bpy.utils.user_resource("DATAFILES", path=f"{__name__}", create=True) def on_change_assets_dir(self, context): if not os.path.isdir(self.assets_dir): butils.ShowMessageBox( title=f"{__name__} Warning", icon="ERROR", - message=("Chosen directory does not exist.", - "Please, reset to default, create it or chose another one.")) + message=( + "Chosen directory does not exist.", + "Please, reset to default, create it or chose another one.", + ), + ) elif not os.access(self.assets_dir, os.W_OK): butils.ShowMessageBox( title=f"{__name__} Warning", icon="ERROR", - message=("Chosen directory is not writable.", - "Please reset to default or chose another one.")) + message=( + "Chosen directory is not writable.", + "Please reset to default or chose another one.", + ), + ) print(f"{__name__}: change assets_dir to {self.assets_dir}") assets_dir: bpy.props.StringProperty( name="Assets Folder", - subtype='DIR_PATH', + subtype="DIR_PATH", default=get_default_assets_dir(), update=on_change_assets_dir, ) @@ -168,9 +174,7 @@ class ABC3D_text_properties(bpy.types.PropertyGroup): return 0 # "" def glyphs_update_callback(self, context): - butils.prepare_text(self.font_name, - self.face_name, - self.text) + butils.prepare_text(self.font_name, self.face_name, self.text) butils.set_text_on_curve(self) def update_callback(self, context): @@ -187,46 +191,40 @@ class ABC3D_text_properties(bpy.types.PropertyGroup): items=font_items_callback, update=font_update_callback, ) - font_name: bpy.props.StringProperty( - update=glyphs_update_callback - ) - face_name: bpy.props.StringProperty( - update=glyphs_update_callback - ) + font_name: bpy.props.StringProperty(update=glyphs_update_callback) + face_name: bpy.props.StringProperty(update=glyphs_update_callback) text_object: bpy.props.PointerProperty(type=bpy.types.Object) - text: bpy.props.StringProperty( - update=glyphs_update_callback - ) + text: bpy.props.StringProperty(update=glyphs_update_callback) letter_spacing: bpy.props.FloatProperty( update=update_callback, name="Letter Spacing", description="Letter Spacing", - options={'ANIMATABLE'}, + options={"ANIMATABLE"}, step=0.01, ) orientation: bpy.props.FloatVectorProperty( update=update_callback, name="Orientation", default=(1.5707963267948966, 0.0, 0.0), # 90 degrees in radians - subtype='EULER', + subtype="EULER", ) translation: bpy.props.FloatVectorProperty( update=update_callback, name="Translation", default=(0.0, 0.0, 0.0), - subtype='TRANSLATION', + subtype="TRANSLATION", ) font_size: bpy.props.FloatProperty( update=update_callback, name="Font Size", default=1.0, - subtype='NONE', + subtype="NONE", ) offset: bpy.props.FloatProperty( update=update_callback, name="Offset", default=0.0, - subtype='NONE', + subtype="NONE", ) compensate_curvature: bpy.props.BoolProperty( update=update_callback, @@ -242,9 +240,11 @@ class ABC3D_text_properties(bpy.types.PropertyGroup): distribution_type: bpy.props.StringProperty() glyphs: bpy.props.CollectionProperty(type=ABC3D_glyph_properties) + class ABC3D_data(bpy.types.PropertyGroup): available_fonts: bpy.props.CollectionProperty( - type=ABC3D_available_font, name="Available fonts") + type=ABC3D_available_font, name="Available fonts" + ) def active_font_index_update(self, context): if len(self.available_fonts) <= self.active_font_index: @@ -255,7 +255,8 @@ class ABC3D_data(bpy.types.PropertyGroup): update=active_font_index_update, ) available_texts: bpy.props.CollectionProperty( - type=ABC3D_text_properties, name="Available texts") + type=ABC3D_text_properties, name="Available texts" + ) def active_text_index_update(self, context): if self.active_text_index != -1: @@ -263,12 +264,15 @@ class ABC3D_data(bpy.types.PropertyGroup): # active_text_index changed. so let's update the selection # check if it is already selected # or perhaps one of the glyphs - if not o.select_get() and not len([c for c in o.children if c.select_get()]) > 0: + if ( + not o.select_get() + and not len([c for c in o.children if c.select_get()]) > 0 + ): bpy.ops.object.select_all(action="DESELECT") o.select_set(True) bpy.context.view_layer.objects.active = o # else: - # print("already selected") + # print("already selected") active_text_index: bpy.props.IntProperty(update=active_text_index_update) @@ -283,15 +287,19 @@ class ABC3D_data(bpy.types.PropertyGroup): default="", maxlen=1024, # update=font_path_update_callback, - subtype="FILE_PATH") + subtype="FILE_PATH", + ) export_dir: bpy.props.StringProperty( name="Export Directory", description=f"The directory in which we will export fonts.\nIf it is blank, we will export to the addon assets path.\nThis is where the fonts are installed.", - subtype="DIR_PATH") + subtype="DIR_PATH", + ) class ABC3D_UL_fonts(bpy.types.UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + def draw_item( + self, context, layout, data, item, icon, active_data, active_propname, index + ): # avoids renaming the item by accident layout.label(text=f"{index}: {item.font_name} {item.face_name}") @@ -300,7 +308,9 @@ class ABC3D_UL_fonts(bpy.types.UIList): class ABC3D_UL_texts(bpy.types.UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + def draw_item( + self, context, layout, data, item, icon, active_data, active_propname, index + ): split = layout.split(factor=0.3) split.label(text="Id: %d" % (item.text_id)) # avoids renaming the item by accident @@ -309,6 +319,7 @@ class ABC3D_UL_texts(bpy.types.UIList): def invoke(self, context, event): pass + class ABC3D_PT_Panel(bpy.types.Panel): bl_label = f"{__name__} panel" bl_category = "ABC3D" @@ -318,16 +329,20 @@ class ABC3D_PT_Panel(bpy.types.Panel): def draw(self, context): layout = self.layout - icon = 'NONE' + icon = "NONE" if len(context.scene.abc3d_data.available_fonts) == 0: - icon = 'ERROR' - layout.row().label(text='no fonts loaded yet') + icon = "ERROR" + layout.row().label(text="no fonts loaded yet") - layout.operator(f"{__name__}.install_font", text='Install new font') - layout.operator(f"{__name__}.load_installed_fonts", - text="load installed fonts", icon=icon) - layout.operator(f"{__name__}.open_asset_directory", - text="open asset directory", icon='FILEBROWSER') + layout.operator(f"{__name__}.install_font", text="Install new font") + layout.operator( + f"{__name__}.load_installed_fonts", text="load installed fonts", icon=icon + ) + layout.operator( + f"{__name__}.open_asset_directory", + text="open asset directory", + icon="FILEBROWSER", + ) class ABC3D_PT_FontList(bpy.types.Panel): @@ -345,16 +360,22 @@ class ABC3D_PT_FontList(bpy.types.Panel): abc3d_data = scene.abc3d_data layout.label(text="Available Fonts") - layout.template_list("ABC3D_UL_fonts", "", abc3d_data, - "available_fonts", abc3d_data, "active_font_index") + 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) + 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}") @@ -363,26 +384,37 @@ class ABC3D_PT_FontList(bpy.types.Panel): box.row().label(text=f"Glyphs:") subbox = box.box() for i in range(0, n_rows + 1): - text = ''.join([f"{u}" for ui, u in enumerate( - available_glyphs) if ui < (i+1) * n and ui >= i * n]) + 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.alignment = "CENTER" row.label(text=text) n_rows = int(len(loaded_glyphs) / n) box.row().label(text=f"Loaded/Used Glyphs:") subbox = box.box() for i in range(0, n_rows + 1): - text = ''.join([f"{u}" for ui, u in enumerate( - loaded_glyphs) if ui < (i+1) * n and ui >= i * n]) + 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 = row.operator( + f"{__name__}.load_font", text="Load all glyphs in memory" + ) oper_lf.font_name = font_name oper_lf.face_name = face_name @@ -398,7 +430,10 @@ class ABC3D_PT_TextPlacement(bpy.types.Panel): @classmethod def poll(self, context): - if type(context.active_object) != type(None) and context.active_object.type == 'CURVE': + if ( + type(context.active_object) != type(None) + and context.active_object.type == "CURVE" + ): self.can_place = True else: self.can_place = False @@ -413,7 +448,7 @@ class ABC3D_PT_TextPlacement(bpy.types.Panel): placerow = layout.row() placerow.enabled = self.can_place - placerow.operator(f"{__name__}.placetext", text='Place Text') + placerow.operator(f"{__name__}.placetext", text="Place Text") if not self.can_place: layout.label(text="Cannot place Text.") layout.label(text="Select a curve as active object.") @@ -445,24 +480,43 @@ class ABC3D_PT_TextManagement(bpy.types.Panel): continue remove_me = True for c in t.text_object.children: - if len(c.users_collection) > 0 and (c.get(f"{utils.prefix()}_linked_textobject")) != type(None) and c.get(f"{utils.prefix()}_linked_textobject") == t.text_id: + if ( + len(c.users_collection) > 0 + and (c.get(f"{utils.prefix()}_linked_textobject")) != type(None) + and c.get(f"{utils.prefix()}_linked_textobject") == t.text_id + ): remove_me = False # not sure how to solve this reliably atm, # we need to reassign the glyph, but also get the proper properties from glyph_properties # these might be there in t.glyphs, but linked to removed objects # or they might be lost - if type(next((g for g in t.glyphs if type(g.glyph_object) == type(None)), None)) == type(None): - g = next((g for g in t.glyphs if type( - g.glyph_object) == type(None)), None) + if type( + next( + ( + g + for g in t.glyphs + if type(g.glyph_object) == type(None) + ), + None, + ) + ) == type(None): + g = next( + ( + g + for g in t.glyphs + if type(g.glyph_object) == type(None) + ), + None, + ) # for g in t.glyphs: - # if type(g) == type(None): - # print("IS NONE") - # if type(g.glyph_object) == type(None): - # print("go IS NONE") - # else: - # if g.glyph_object == c: - # # print(g.glyph_object.name) - # pass + # if type(g) == type(None): + # print("IS NONE") + # if type(g.glyph_object) == type(None): + # print("go IS NONE") + # else: + # if g.glyph_object == c: + # # print(g.glyph_object.name) + # pass if remove_me: remove_list.append(i) @@ -474,6 +528,7 @@ class ABC3D_PT_TextManagement(bpy.types.Panel): 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") @@ -487,8 +542,10 @@ class ABC3D_PT_TextManagement(bpy.types.Panel): for i, t in enumerate(abc3d_data.available_texts): if context.active_object == t.text_object: active_text_index = i - if (hasattr(context.active_object, "parent") and - context.active_object.parent == t.text_object): + if ( + hasattr(context.active_object, "parent") + and context.active_object.parent == t.text_object + ): active_text_index = i if active_text_index != abc3d_data.active_text_index: @@ -506,10 +563,15 @@ class ABC3D_PT_TextManagement(bpy.types.Panel): abc3d_data = scene.abc3d_data layout.label(text="Text Objects") - layout.template_list("ABC3D_UL_texts", "", abc3d_data, - "available_texts", abc3d_data, "active_text_index") - layout.row().operator( - f"{__name__}.remove_text", text="Remove Textobject") + layout.template_list( + "ABC3D_UL_texts", + "", + abc3d_data, + "available_texts", + abc3d_data, + "active_text_index", + ) + layout.row().operator(f"{__name__}.remove_text", text="Remove Textobject") class ABC3D_PT_FontCreation(bpy.types.Panel): @@ -537,13 +599,17 @@ class ABC3D_PT_FontCreation(bpy.types.Panel): box = layout.box() box.label(text="metrics") box.row().operator( - f"{__name__}.add_default_metrics", text='Add Default Metrics') - box.row().operator(f"{__name__}.remove_metrics", text='Remove Metrics') - box.row().operator(f"{__name__}.align_metrics", text='Align Metrics') + f"{__name__}.add_default_metrics", text="Add Default Metrics" + ) + box.row().operator(f"{__name__}.remove_metrics", text="Remove Metrics") + box.row().operator(f"{__name__}.align_metrics", text="Align Metrics") box.row().operator( - f"{__name__}.align_metrics_to_active_object", text='Align Metrics to Active Object') + f"{__name__}.align_metrics_to_active_object", + text="Align Metrics to Active Object", + ) layout.row().operator( - f"{__name__}.temporaryhelper", text='Debug Function Do Not Use') + f"{__name__}.temporaryhelper", text="Debug Function Do Not Use" + ) class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): @@ -564,31 +630,31 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): return None # def font_items_callback(self, context): - # items = [] - # fonts = Font.get_loaded_fonts_and_faces() - # for f in fonts: - # items.append((f"{f[0]} {f[1]}", f"{f[0]} {f[1]}", "")) - # return items + # items = [] + # fonts = Font.get_loaded_fonts_and_faces() + # for f in fonts: + # items.append((f"{f[0]} {f[1]}", f"{f[0]} {f[1]}", "")) + # return items # def font_default_callback(self, context): - # t = self.get_active_text_properties(self) - # if type(t) != type(None): - # return f"{t.font_name} {t.face_name}" - # else: - # return None + # t = self.get_active_text_properties(self) + # if type(t) != type(None): + # return f"{t.font_name} {t.face_name}" + # else: + # return None # def font_update_callback(self, context): - # font_name, face_name = self.font.split(" ") - # t = self.get_active_text_properties(self) - # t.font_name = font_name - # t.face_name = face_name - # butils.set_text_on_curve(t) + # font_name, face_name = self.font.split(" ") + # t = self.get_active_text_properties(self) + # t.font_name = font_name + # t.face_name = face_name + # butils.set_text_on_curve(t) # font: bpy.props.EnumProperty( - # items=font_items_callback, - # default=font_default_callback, - # update=font_update_callback, - # ) + # items=font_items_callback, + # default=font_default_callback, + # update=font_update_callback, + # ) @classmethod def poll(self, context): @@ -622,16 +688,18 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): class ABC3D_OT_InstallFont(bpy.types.Operator): """Install or load Fontfile from path above. -(Format must be *.glb or *.gltf)""" + (Format must be *.glb or *.gltf)""" + bl_idname = f"{__name__}.install_font" bl_label = "Load Font" - bl_options = {'REGISTER', 'UNDO'} + bl_options = {"REGISTER", "UNDO"} def font_path_update_callback(self, context): - if os.path.exists(self.font_path): - print(f"{self.font_path} does exist") + font_path = butils.bpy_to_abspath(self.font_path) + if os.path.exists(font_path): + print(f"font_path_update: {font_path} does exist") else: - print(f"{self.font_path} does not exist") + print(f"font_path_update: {font_path} does not exist") font_path: bpy.props.StringProperty( name="Font path", @@ -639,7 +707,8 @@ class ABC3D_OT_InstallFont(bpy.types.Operator): default="", maxlen=1024, update=font_path_update_callback, - subtype="FILE_PATH") + subtype="FILE_PATH", + ) install_in_assets: bpy.props.BoolProperty( name="install in assets", @@ -647,9 +716,11 @@ class ABC3D_OT_InstallFont(bpy.types.Operator): default=True, ) - load_into_memory: bpy.props.BoolProperty(name="load font data into memory", - description="if false, it will load font data on demand", - default=False) + load_into_memory: bpy.props.BoolProperty( + name="load font data into memory", + description="if false, it will load font data on demand", + default=False, + ) def draw(self, context): abc3d_data = context.scene.abc3d_data @@ -659,8 +730,7 @@ class ABC3D_OT_InstallFont(bpy.types.Operator): layout.row().prop(self, "install_in_assets") if not self.install_in_assets and not self.load_into_memory: layout.label(text="If the fontfile is not installed,") - layout.label( - text="and the font is not loaded in memory completely,") + layout.label(text="and the font is not loaded in memory completely,") layout.label(text="the fontfile should not be moved.") layout.row().prop(self, "load_into_memory") if self.load_into_memory: @@ -672,14 +742,14 @@ class ABC3D_OT_InstallFont(bpy.types.Operator): def invoke(self, context, event): # self.font_path = butils.bpy_to_abspath(self.font_path) # if not os.path.exists(self.font_path): - # bpy.app.timers.register(lambda: butils.ShowMessageBox( - # title=f"{__name__} Warning", - # icon="ERROR", - # message=[ - # f"We believe the font path ({self.font_path}) does not exist.", - # f"Did you select your fontfile in the field above the 'Install new font'-button?", - # ], - # ), first_interval=0.1) + # bpy.app.timers.register(lambda: butils.ShowMessageBox( + # title=f"{__name__} Warning", + # icon="ERROR", + # message=[ + # f"We believe the font path ({self.font_path}) does not exist.", + # f"Did you select your fontfile in the field above the 'Install new font'-button?", + # ], + # ), first_interval=0.1) return context.window_manager.invoke_props_dialog(self) def execute(self, context): @@ -697,13 +767,14 @@ class ABC3D_OT_InstallFont(bpy.types.Operator): f"If this is an error, please let us know.", ], ) - return {'CANCELLED'} + return {"CANCELLED"} if self.install_in_assets: preferences = getPreferences(context) filename = os.path.basename(font_path) target = os.path.join(preferences.assets_dir, "fonts", filename) import shutil + os.makedirs(os.path.dirname(target), exist_ok=True) shutil.copyfile(font_path, target) # def register_load(target, load=False): @@ -718,40 +789,47 @@ class ABC3D_OT_InstallFont(bpy.types.Operator): if self.load_into_memory: butils.load_font_from_filepath(font_path) - return {'FINISHED'} + return {"FINISHED"} class ABC3D_OT_OpenAssetDirectory(bpy.types.Operator): """Open Asset Directory""" + bl_idname = f"{__name__}.open_asset_directory" bl_label = "Opens asset directory." - bl_options = {'REGISTER', 'UNDO'} + bl_options = {"REGISTER", "UNDO"} def execute(self, context): preferences = getPreferences(context) directory = os.path.realpath(preferences.assets_dir) if os.path.exists(directory): utils.open_file_browser(directory) - return {'FINISHED'} + return {"FINISHED"} else: butils.ShowMessageBox( title=f"{__name__} Warning", icon="ERROR", - message=("Asset directory does not exist.", - f"Command failed trying to access '{directory}'.", - "Please, make sure it exists or chose another directory.")) - return {'CANCELLED'} + message=( + "Asset directory does not exist.", + f"Command failed trying to access '{directory}'.", + "Please, make sure it exists or chose another directory.", + ), + ) + return {"CANCELLED"} class ABC3D_OT_LoadInstalledFonts(bpy.types.Operator): """Load installed fontfiles from datapath.""" + bl_idname = f"{__name__}.load_installed_fonts" bl_label = "Loading installed Fonts." - bl_options = {'REGISTER', 'UNDO'} + 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) + 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 @@ -773,22 +851,19 @@ class ABC3D_OT_LoadInstalledFonts(bpy.types.Operator): butils.load_installed_fonts() else: butils.register_installed_fonts() - butils.ShowMessageBox("Loading Fonts", - 'INFO', - "Updating Data Structures.") + butils.ShowMessageBox("Loading Fonts", "INFO", "Updating Data Structures.") butils.update_available_fonts() - butils.ShowMessageBox("Loading Fonts", - 'INFO', - "Done loading installed fonts.") + butils.ShowMessageBox("Loading Fonts", "INFO", "Done loading installed fonts.") - return {'FINISHED'} + return {"FINISHED"} class ABC3D_OT_LoadFont(bpy.types.Operator): """Load all glyphs from a specific font in memory.\nThis can take a while and slow down Blender.""" + bl_idname = f"{__name__}.load_font" bl_label = "Loading Font." - bl_options = {'REGISTER', 'UNDO'} + bl_options = {"REGISTER", "UNDO"} font_name: bpy.props.StringProperty() face_name: bpy.props.StringProperty() @@ -797,62 +872,72 @@ class ABC3D_OT_LoadFont(bpy.types.Operator): filepaths = Font.fonts[self.font_name].faces[self.face_name].filepaths for f in filepaths: butils.load_font_from_filepath(f) - return {'FINISHED'} + return {"FINISHED"} class ABC3D_OT_AddDefaultMetrics(bpy.types.Operator): """Add default metrics to selected objects""" + bl_idname = f"{__name__}.add_default_metrics" bl_label = "Add default metrics" - bl_options = {'REGISTER', 'UNDO'} + bl_options = {"REGISTER", "UNDO"} def execute(self, context): objects = bpy.context.selected_objects butils.add_default_metrics_to_objects(objects) - return {'FINISHED'} + return {"FINISHED"} class ABC3D_OT_RemoveMetrics(bpy.types.Operator): """Remove metrics from selected objects""" + bl_idname = f"{__name__}.remove_metrics" bl_label = "Remove metrics" - bl_options = {'REGISTER', 'UNDO'} + bl_options = {"REGISTER", "UNDO"} def execute(self, context): objects = bpy.context.selected_objects butils.remove_metrics_from_objects(objects) - return {'FINISHED'} + return {"FINISHED"} class ABC3D_OT_AlignMetricsToActiveObject(bpy.types.Operator): - """Align metrics of selected objects to metrics of active object""" + """Align metrics of selected objects to metrics of active object. + + The metrics of the active object are not changed and is taken as a reference for all other objects. + """ + bl_idname = f"{__name__}.align_metrics_to_active_object" bl_label = "Align metrics to active object" - bl_options = {'REGISTER', 'UNDO'} + bl_options = {"REGISTER", "UNDO"} def execute(self, context): objects = bpy.context.selected_objects butils.align_metrics_of_objects_to_active_object(objects) - return {'FINISHED'} + return {"FINISHED"} class ABC3D_OT_AlignMetrics(bpy.types.Operator): - """Align metrics of selected objects to each other""" + """Align metrics of selected objects to each other. + + The metrics of all objects are merged and expanded to fit for all objects.""" + bl_idname = f"{__name__}.align_metrics" bl_label = "Align metrics" - bl_options = {'REGISTER', 'UNDO'} + bl_options = {"REGISTER", "UNDO"} def execute(self, context): objects = bpy.context.selected_objects butils.align_metrics_of_objects(objects) - return {'FINISHED'} + return {"FINISHED"} class ABC3D_OT_TemporaryHelper(bpy.types.Operator): """Temporary Helper ABC3D\nThis could do anything.\nIt's just there to make random functions available for testing.""" + bl_idname = f"{__name__}.temporaryhelper" bl_label = "Temp Font" - bl_options = {'REGISTER', 'UNDO'} + bl_options = {"REGISTER", "UNDO"} def execute(self, context): global shared @@ -873,19 +958,21 @@ class ABC3D_OT_TemporaryHelper(bpy.types.Operator): # butils.add_metrics_obj_from_bound_box(o, metrics) # bpy.app.timers.register(lambda: butils.remove_metrics_from_objects(objects), first_interval=5) - return {'FINISHED'} + return {"FINISHED"} class ABC3D_OT_RemoveText(bpy.types.Operator): """Remove Text 3D""" + bl_idname = f"{__name__}.remove_text" bl_label = "Remove Text" - bl_options = {'REGISTER', 'UNDO'} + bl_options = {"REGISTER", "UNDO"} remove_objects: bpy.props.BoolProperty( name="Remove Objects", description="Remove both ABC3D text functionality and the objects/meshes", - default=True) + default=True, + ) def invoke(self, context, event): wm = context.window_manager @@ -897,8 +984,9 @@ class ABC3D_OT_RemoveText(bpy.types.Operator): butils.ShowMessageBox( title="No text selected", message=("Please select a text."), - icon='GHOST_ENABLED') - return {'CANCELLED'} + icon="GHOST_ENABLED", + ) + return {"CANCELLED"} i = abc3d_data.active_text_index if type(abc3d_data.available_texts[i].text_object) != type(None): @@ -907,6 +995,7 @@ class ABC3D_OT_RemoveText(bpy.types.Operator): def delif(o, p): if p in o: del o[p] + delif(mom, f"{utils.prefix()}_type") delif(mom, f"{utils.prefix()}_linked_textobject") delif(mom, f"{utils.prefix()}_font_name") @@ -925,14 +1014,15 @@ class ABC3D_OT_RemoveText(bpy.types.Operator): abc3d_data.available_texts.remove(i) - return {'FINISHED'} + return {"FINISHED"} class ABC3D_OT_PlaceText(bpy.types.Operator): """Place Text 3D on active object""" + bl_idname = f"{__name__}.placetext" bl_label = "Place Text" - bl_options = {'REGISTER', 'UNDO'} + bl_options = {"REGISTER", "UNDO"} def font_items_callback(self, context): items = [] @@ -946,15 +1036,9 @@ class ABC3D_OT_PlaceText(bpy.types.Operator): self.font_name = font_name self.face_name = face_name - font_name: bpy.props.StringProperty( - options={'HIDDEN'} - ) - face_name: bpy.props.StringProperty( - options={'HIDDEN'} - ) - font: bpy.props.EnumProperty(items=font_items_callback, - update=font_update_callback - ) + font_name: bpy.props.StringProperty(options={"HIDDEN"}) + face_name: bpy.props.StringProperty(options={"HIDDEN"}) + font: bpy.props.EnumProperty(items=font_items_callback, update=font_update_callback) text: bpy.props.StringProperty( name="Text", description="The text.", @@ -974,22 +1058,22 @@ class ABC3D_OT_PlaceText(bpy.types.Operator): font_size: bpy.props.FloatProperty( name="Font Size", default=1.0, - subtype='NONE', + subtype="NONE", ) offset: bpy.props.FloatProperty( name="Offset", default=0.0, - subtype='NONE', + subtype="NONE", ) translation: bpy.props.FloatVectorProperty( name="Translation", default=(0.0, 0.0, 0.0), - subtype='TRANSLATION', + subtype="TRANSLATION", ) orientation: bpy.props.FloatVectorProperty( name="Orientation", default=(1.5707963267948966, 0.0, 0.0), # 90 degrees in radians - subtype='EULER', + subtype="EULER", ) def invoke(self, context, event): @@ -1010,7 +1094,7 @@ class ABC3D_OT_PlaceText(bpy.types.Operator): # font_name = font.font_name # face_name = font.face_name - distribution_type = 'DEFAULT' + distribution_type = "DEFAULT" text_id = 0 for i, tt in enumerate(abc3d_data.available_texts): @@ -1019,18 +1103,18 @@ class ABC3D_OT_PlaceText(bpy.types.Operator): t = abc3d_data.available_texts.add() # If you wish to set a value and not fire an update, set the id property. # A property defined via bpy.props for example ob.prop is stored as ob["prop"] once set to non default. - t['text_id'] = text_id + t["text_id"] = text_id # t['font'] = self.font # enums want to be set as attribute - t['font_name'] = self.font_name - t['face_name'] = self.face_name + t["font_name"] = self.font_name + t["face_name"] = self.face_name t.text_object = selected - t['text'] = self.text - t['letter_spacing'] = self.letter_spacing - t['font_size'] = self.font_size - t['offset'] = self.offset - t['translation'] = self.translation - t['orientation'] = self.orientation - t['distribution_type'] = distribution_type + t["text"] = self.text + t["letter_spacing"] = self.letter_spacing + t["font_size"] = self.font_size + t["offset"] = self.offset + t["translation"] = self.translation + t["orientation"] = self.orientation + t["distribution_type"] = distribution_type t.font = self.font # enums want to be set as attribute # this also calls the update function # so we don't need to prepare/set again @@ -1046,17 +1130,20 @@ class ABC3D_OT_PlaceText(bpy.types.Operator): message=( "Please select an object.", "It will be used to put the type on.", - "Thank you :)"), - icon='GHOST_ENABLED') + "Thank you :)", + ), + icon="GHOST_ENABLED", + ) - return {'FINISHED'} + return {"FINISHED"} class ABC3D_OT_ToggleABC3DCollection(bpy.types.Operator): """Toggle ABC3D Collection""" + bl_idname = f"{__name__}.toggle_abc3d_collection" bl_label = "Toggle Collection visibility" - bl_options = {'REGISTER', 'UNDO'} + bl_options = {"REGISTER", "UNDO"} def execute(self, context): scene = context.scene @@ -1064,56 +1151,68 @@ class ABC3D_OT_ToggleABC3DCollection(bpy.types.Operator): if fontcollection is None: self.report( - {'INFO'}, f"{bl_info['name']}: There is no collection. Did you use or create any glyphs yet?") + {"INFO"}, + f"{bl_info['name']}: There is no collection. Did you use or create any glyphs yet?", + ) elif scene.collection.children.find(fontcollection.name) < 0: scene.collection.children.link(fontcollection) - self.report({'INFO'}, f"{bl_info['name']}: show collection") + self.report({"INFO"}, f"{bl_info['name']}: show collection") else: scene.collection.children.unlink(fontcollection) - self.report({'INFO'}, f"{bl_info['name']}: hide collection") + self.report({"INFO"}, f"{bl_info['name']}: hide collection") - return {'FINISHED'} + return {"FINISHED"} class ABC3D_OT_SaveFontToFile(bpy.types.Operator): """Save font to file""" + bl_idname = f"{__name__}.save_font_to_file" bl_label = "Save Font" - bl_options = {'REGISTER', 'UNDO'} + bl_options = {"REGISTER", "UNDO"} def invoke(self, context, event): wm = context.window_manager preferences = getPreferences(context) abc3d_data = context.scene.abc3d_data if abc3d_data.export_dir == "": - abc3d_data.export_dir = os.path.join( - preferences.assets_dir, "fonts") + abc3d_data.export_dir = os.path.join(preferences.assets_dir, "fonts") return wm.invoke_props_dialog(self) def draw(self, context): abc3d_data = context.scene.abc3d_data layout = self.layout layout.label(text="Available Fonts") - layout.template_list("ABC3D_UL_fonts", "", abc3d_data, - "available_fonts", abc3d_data, "active_font_index") + layout.template_list( + "ABC3D_UL_fonts", + "", + abc3d_data, + "available_fonts", + abc3d_data, + "active_font_index", + ) available_font = abc3d_data.available_fonts[abc3d_data.active_font_index] font_name = available_font.font_name face_name = available_font.face_name - loaded_glyphs = sorted( - Font.fonts[font_name].faces[face_name].loaded_glyphs) + loaded_glyphs = sorted(Font.fonts[font_name].faces[face_name].loaded_glyphs) n = 16 n_rows = int(len(loaded_glyphs) / n) box = layout.box() box.row().label(text=f"Glyphs to be exported:") subbox = box.box() for i in range(0, n_rows + 1): - text = ''.join([f"{u}" for ui, u in enumerate( - loaded_glyphs) if ui < (i+1) * n and ui >= i * n]) + 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') + layout.prop(abc3d_data, "export_dir") def execute(self, context): global shared @@ -1124,22 +1223,21 @@ class ABC3D_OT_SaveFontToFile(bpy.types.Operator): # check if all is good to proceed if fontcollection is None: - self.report({'INFO'}, f"{bl_info['name']}: There is no collection") - return {'CANCELLED'} + self.report({"INFO"}, f"{bl_info['name']}: There is no collection") + return {"CANCELLED"} if abc3d_data.active_font_index < 0: - self.report( - {'INFO'}, f"{bl_info['name']}: There is no active font") - return {'CANCELLED'} + self.report({"INFO"}, f"{bl_info['name']}: There is no active font") + return {"CANCELLED"} if len(abc3d_data.available_fonts) <= abc3d_data.active_font_index: - self.report( - {'INFO'}, f"{bl_info['name']}: Active font is not available") - return {'CANCELLED'} + self.report({"INFO"}, f"{bl_info['name']}: Active font is not available") + return {"CANCELLED"} # save state to restore later - was_fontcollection_linked = scene.collection.children.find( - fontcollection.name) >= 0 + was_fontcollection_linked = ( + scene.collection.children.find(fontcollection.name) >= 0 + ) was_selection = [] for obj in bpy.context.selected_objects: was_selection.append(obj) @@ -1152,11 +1250,13 @@ class ABC3D_OT_SaveFontToFile(bpy.types.Operator): # print(selected_font.font_name) self.report( - {'INFO'}, f"{bl_info['name']}: {selected_font.font_name} {selected_font.face_name}") + {"INFO"}, + f"{bl_info['name']}: {selected_font.font_name} {selected_font.face_name}", + ) preferences = getPreferences(context) print(f"assets folder: {preferences.assets_dir}") - bpy.ops.scene.new(type='FULL_COPY') + bpy.ops.scene.new(type="FULL_COPY") linked_collections = bpy.context.scene.collection.children.values() for c in linked_collections: @@ -1192,13 +1292,12 @@ class ABC3D_OT_SaveFontToFile(bpy.types.Operator): filepath=filepath, check_existing=False, # GLB or GLTF_SEPARATE (also change filepath) - export_format='GLB', + export_format="GLB", export_extras=True, use_selection=True, use_active_scene=True, ) - bpy.app.timers.register( - lambda: bpy.ops.scene.delete(), first_interval=1) + bpy.app.timers.register(lambda: bpy.ops.scene.delete(), first_interval=1) # bpy.ops.scene.delete() # restore() @@ -1208,20 +1307,24 @@ class ABC3D_OT_SaveFontToFile(bpy.types.Operator): if obj["font_name"] == selected_font.font_name: if butils.is_metrics_object(obj): butils.remove_faces_from_metrics(obj) - bpy.app.timers.register(lambda: remove_faces(), first_interval=2) - self.report({'INFO'}, f"did it") - return {'FINISHED'} + bpy.app.timers.register(lambda: remove_faces(), first_interval=2) + self.report({"INFO"}, f"did it") + + return {"FINISHED"} + + # keep = ['io_anim_bvh', 'io_curve_svg', 'io_mesh_stl', 'io_mesh_uv_layout', 'io_scene_fbx', 'io_scene_gltf2', 'io_scene_x3d', 'cycles', 'pose_library', 'abc3d'] # for addon in keep: - # bpy.ops.preferences.addon_enable(module=addon) +# bpy.ops.preferences.addon_enable(module=addon) class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): """Create Font from selected objects""" + bl_idname = f"{__name__}.create_font_from_objects" bl_label = "Create Font" - bl_options = {'REGISTER', 'UNDO'} + bl_options = {"REGISTER", "UNDO"} font_name: bpy.props.StringProperty( default="NM_Origin", @@ -1267,8 +1370,8 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): scale_y = 0.5 row = layout.row() row.scale_y = scale_y - row.label( - text="Watch out, follow convention in naming your meshes:") + row.label(text="Autodetecting names per glyph.") + row.label(text="Watch out, follow convention in naming your meshes:") row = layout.row() row.scale_y = scale_y row.label(text="'__'") @@ -1311,17 +1414,18 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): for k in Font.known_misspellings: character = "" if Font.known_misspellings[k] in Font.name_to_glyph_d: - character = f" ({Font.name_to_glyph_d[Font.known_misspellings[k]]})" + character = ( + f" ({Font.name_to_glyph_d[Font.known_misspellings[k]]})" + ) row = layout.row() row.scale_y = 0.5 - row.label( - text=f"{k} → {Font.known_misspellings[k]}{character}") + row.label(text=f"{k} → {Font.known_misspellings[k]}{character}") def execute(self, context): print(f"executing {self.bl_idname}") if len(context.selected_objects) == 0: print(f"cancelled {self.bl_idname} - no objects selected") - return {'CANCELLED'} + return {"CANCELLED"} global shared scene = bpy.context.scene abc3d_data = scene.abc3d_data @@ -1351,7 +1455,7 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): o.name = Font.fix_glyph_name_misspellings(o.name) # name = re.sub(regex, "", o.name) # glyph_id = Font.name_to_glyph(name) - name = o.name.split('_')[0] + name = o.name.split("_")[0] glyph_id = Font.name_to_glyph(name) if type(glyph_id) != type(None): @@ -1359,20 +1463,15 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): o["font_name"] = font_name o["face_name"] = face_name # butils.apply_all_transforms(o) - butils.move_in_fontcollection( - o, - fontcollection) + butils.move_in_fontcollection(o, fontcollection) Font.add_glyph( - font_name, - face_name, - glyph_id, - bpy.types.PointerProperty(o)) + font_name, face_name, glyph_id, bpy.types.PointerProperty(o) + ) # TODO: is there a better way to iterate over a CollectionProperty? found = False for f in abc3d_data.available_fonts.values(): - if (f.font_name == font_name - and f.face_name == face_name): + if f.font_name == font_name and f.face_name == face_name: found = True break if not found: @@ -1381,12 +1480,10 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): f.face_name = face_name else: - print( - f"import warning: did not understand glyph {name}") - self.report( - {'INFO'}, f"did not understand glyph {name}") + print(f"import warning: did not understand glyph {name}") + self.report({"INFO"}, f"did not understand glyph {name}") - return {'FINISHED'} + return {"FINISHED"} class ABC3D_OT_Reporter(bpy.types.Operator): @@ -1404,10 +1501,10 @@ class ABC3D_OT_Reporter(bpy.types.Operator): def execute(self, context): # this is where I send the message - self.report({'INFO'}, 'whatever') + self.report({"INFO"}, "whatever") for i in range(0, 10): - butils.ShowMessageBox('whatever', 'INFO', 'INFO') - return {'FINISHED'} + butils.ShowMessageBox("whatever", "INFO", "INFO") + return {"FINISHED"} classes = ( @@ -1459,7 +1556,7 @@ def compare_text_object_with_object(t, o, strict=False): if strict: return False # for p in t.keys(): - # if + # if return True @@ -1469,11 +1566,12 @@ def detect_text(): for o in scene.objects: if o[f"{utils.prefix()}_type"] == "textobject": linked_textobject = int(o[f"{utils.prefix()}_linked_textobject"]) - if len(abc3d_data.available_texts) > linked_textobject \ - and abc3d_data.available_texts[linked_textobject].text_object == o: + if ( + len(abc3d_data.available_texts) > linked_textobject + and abc3d_data.available_texts[linked_textobject].text_object == o + ): t = abc3d_data.available_texts[linked_textobject] - a = test_availability( - o["font_name"], o["face_name"], o["text"]) + a = test_availability(o["font_name"], o["face_name"], o["text"]) butils.transfer_blender_object_to_text_properties(o, t) @@ -1481,21 +1579,24 @@ def load_used_glyphs(): scene = bpy.context.scene abc3d_data = scene.abc3d_data for t in abc3d_data.available_texts: - a = Font.test_availability(t.font_name, - t.face_name, - t.text) + a = Font.test_availability(t.font_name, t.face_name, t.text) if type(a) == type(int()): if a == Font.MISSING_FONT: - butils.ShowMessageBox("Missing Font", - "ERROR", - [f"Font {t.font_name} is missing.", - "Do you have it installed?"]) + butils.ShowMessageBox( + "Missing Font", + "ERROR", + [f"Font {t.font_name} is missing.", "Do you have it installed?"], + ) if a is Font.MISSING_FACE: - butils.ShowMessageBox("Missing FontFace", - "ERROR", - [f"Font {t.font_name} is there,", - f"but the FontFace {t.face_name} is missing,", - "Do you have it installed?"]) + butils.ShowMessageBox( + "Missing FontFace", + "ERROR", + [ + f"Font {t.font_name} is there,", + f"but the FontFace {t.face_name} is missing,", + "Do you have it installed?", + ], + ) elif len(a["maybe"]) > 0: for fp in a["filepaths"]: butils.load_font_from_filepath(fp, a["maybe"]) @@ -1521,11 +1622,15 @@ def on_frame_changed(self, dummy): # TODO PERFORMANCE: only on demand butils.set_text_on_curve(t) + depsgraph_updates_locked = False + + def unlock_depsgraph_updates(): global depsgraph_updates_locked depsgraph_updates_locked = False + def lock_depsgraph_updates(): global depsgraph_updates_locked depsgraph_updates_locked = True @@ -1533,25 +1638,38 @@ def lock_depsgraph_updates(): bpy.app.timers.unregister(unlock_depsgraph_updates) bpy.app.timers.register(unlock_depsgraph_updates, first_interval=1) + import time + + @persistent def on_depsgraph_update(scene, depsgraph): global depsgraph_updates_locked if not bpy.context.mode.startswith("EDIT") and not depsgraph_updates_locked: for u in depsgraph.updates: - if f"{utils.prefix()}_linked_textobject" in u.id.keys() \ - and f"{utils.prefix()}_type" in u.id.keys() \ - and u.id[f"{utils.prefix()}_type"] == 'textobject': + if ( + f"{utils.prefix()}_linked_textobject" in u.id.keys() + and f"{utils.prefix()}_type" in u.id.keys() + and u.id[f"{utils.prefix()}_type"] == "textobject" + ): linked_textobject = u.id[f"{utils.prefix()}_linked_textobject"] - if u.is_updated_geometry and len(scene.abc3d_data.available_texts) > linked_textobject: + if ( + u.is_updated_geometry + and len(scene.abc3d_data.available_texts) > linked_textobject + ): lock_depsgraph_updates() + def later(): - if not "lock_depsgraph_update_ntimes" in scene.abc3d_data \ - or scene.abc3d_data["lock_depsgraph_update_ntimes"] <= 0: + if ( + not "lock_depsgraph_update_ntimes" in scene.abc3d_data + or scene.abc3d_data["lock_depsgraph_update_ntimes"] <= 0 + ): butils.set_text_on_curve( - scene.abc3d_data.available_texts[linked_textobject]) + scene.abc3d_data.available_texts[linked_textobject] + ) elif scene.abc3d_data["lock_depsgraph_update_ntimes"] > 0: - scene.abc3d_data['lock_depsgraph_update_ntimes'] -= 1 + scene.abc3d_data["lock_depsgraph_update_ntimes"] -= 1 + butils.run_in_main_thread(later) @@ -1609,5 +1727,5 @@ def unregister(): print(f"UNREGISTER {utils.prefix()}") -if __name__ == '__main__': +if __name__ == "__main__": register() diff --git a/butils.py b/butils.py index b9488ba..d3b6940 100644 --- a/butils.py +++ b/butils.py @@ -22,17 +22,20 @@ else: execution_queue = queue.Queue() + # This function can safely be called in another thread. # The function will be executed when the timer runs the next time. def run_in_main_thread(function): execution_queue.put(function) + def execute_queued_functions(): while not execution_queue.empty(): function = execution_queue.get() function() return 1.0 + def apply_all_transforms(obj): mb = obj.matrix_basis if hasattr(obj.data, "transform"): @@ -41,16 +44,18 @@ def apply_all_transforms(obj): c.matrix_local = mb @ c.matrix_local 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 + 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 + # Ensure it's a curve object # TODO: no raising, please -def get_curve_length(curve_obj, resolution = -1): +def get_curve_length(curve_obj, resolution=-1): total_length = 0 curve = curve_obj.data @@ -58,16 +63,20 @@ def get_curve_length(curve_obj, resolution = -1): # Loop through all splines in the curve for spline in curve.splines: total_length = total_length + spline.calc_length(resolution=resolution) - + return total_length -def get_curve_line_lengths(curve_obj, resolution = -1): + +def get_curve_line_lengths(curve_obj, resolution=-1): lengths = [] for spline in curve_obj.data.splines: lengths.append(spline.calc_length(resolution=resolution)) return lengths -def get_next_line_advance(curve_obj, current_advance, previous_glyph_advance, resolution = -1): + +def get_next_line_advance( + curve_obj, current_advance, previous_glyph_advance, resolution=-1 +): curve_line_lengths = get_curve_line_lengths(curve_obj, resolution) total_length = 0 for cll in curve_line_lengths: @@ -76,30 +85,37 @@ def get_next_line_advance(curve_obj, current_advance, previous_glyph_advance, re return total_length return current_advance + def calc_point_on_bezier(bezier_point_1, bezier_point_2, t): p1 = bezier_point_1.co h1 = bezier_point_1.handle_right p2 = bezier_point_2.co h2 = bezier_point_2.handle_left - return ((1 - t)**3) * p1 + (3 * t * (1 - t)**2) * h1 + (3 * (t**2) * (1 - t)) * h2 + (t**3) * p2 + return ( + ((1 - t) ** 3) * p1 + + (3 * t * (1 - t) ** 2) * h1 + + (3 * (t**2) * (1 - t)) * h2 + + (t**3) * p2 + ) + # same in slightly more lines # result is equal, performance minimally better perhaps? # def calc_point_on_bezier(bezier_point_1, bezier_point_2, ratio): - # startPoint = bezier_point_1.co - # controlPoint1 = bezier_point_1.handle_right - # controlPoint2 = bezier_point_2.handle_left - # endPoint = bezier_point_2.co +# startPoint = bezier_point_1.co +# controlPoint1 = bezier_point_1.handle_right +# controlPoint2 = bezier_point_2.handle_left +# endPoint = bezier_point_2.co - # remainder = 1 - ratio - # ratioSquared = ratio * ratio - # remainderSquared = remainder * remainder - # startPointMultiplier = remainderSquared * remainder - # controlPoint1Multiplier = remainderSquared * ratio * 3 - # controlPoint2Multiplier = ratioSquared * remainder * 3 - # endPointMultiplier = ratioSquared * ratio +# remainder = 1 - ratio +# ratioSquared = ratio * ratio +# remainderSquared = remainder * remainder +# startPointMultiplier = remainderSquared * remainder +# controlPoint1Multiplier = remainderSquared * ratio * 3 +# controlPoint2Multiplier = ratioSquared * remainder * 3 +# endPointMultiplier = ratioSquared * ratio - # return startPoint * startPointMultiplier + controlPoint1 * controlPoint1Multiplier + controlPoint2 * controlPoint2Multiplier + endPoint * endPointMultiplier +# return startPoint * startPointMultiplier + controlPoint1 * controlPoint1Multiplier + controlPoint2 * controlPoint2Multiplier + endPoint * endPointMultiplier def calc_tangent_on_bezier(bezier_point_1, bezier_point_2, t): @@ -108,15 +124,22 @@ def calc_tangent_on_bezier(bezier_point_1, bezier_point_2, t): p2 = bezier_point_2.co h2 = bezier_point_2.handle_left return ( - (-3 * (1 - t)**2) * p1 + (-6 * t * (1 - t) + 3 * (1 - t)**2) * h1 + - (-3 * (t**2) + 6 * t * (1 - t)) * h2 + (3 * t**2) * p2 - ).normalized() + (-3 * (1 - t) ** 2) * p1 + + (-6 * t * (1 - t) + 3 * (1 - t) ** 2) * h1 + + (-3 * (t**2) + 6 * t * (1 - t)) * h2 + + (3 * t**2) * p2 + ).normalized() + from math import acos, pi, radians, sqrt -def align_rotations_auto_pivot(mask, input_rotations, vectors, factors, local_main_axis): - output_rotations = [mathutils.Matrix().to_3x3() for _ in range(len(input_rotations))] +def align_rotations_auto_pivot( + mask, input_rotations, vectors, factors, local_main_axis +): + output_rotations = [ + mathutils.Matrix().to_3x3() for _ in range(len(input_rotations)) + ] for i in mask: vector = mathutils.Vector(vectors[i]).normalized() @@ -135,10 +158,12 @@ def align_rotations_auto_pivot(mask, input_rotations, vectors, factors, local_ma if rotation_axis.length < 1e-6: # Vectors are linearly dependent, fallback to another axis rotation_axis = (old_axis + mathutils.Matrix().to_3x3().col[2]).normalized() - + if rotation_axis.length < 1e-6: # This is now guaranteed to not be zero - rotation_axis = (-(old_axis) + mathutils.Matrix().to_3x3().col[1]).normalized() + rotation_axis = ( + -(old_axis) + mathutils.Matrix().to_3x3().col[1] + ).normalized() # full_angle = radians(sqrt((4 * pow(input_rotation.to_quaternion().dot(mathutils.Quaternion(vectors[i].normalized())), 2) - 3))) # dot = old_axis.dot(new_axis) @@ -153,8 +178,9 @@ def align_rotations_auto_pivot(mask, input_rotations, vectors, factors, local_ma return [mat.to_4x4() for mat in output_rotations] + def calc_bezier_length(bezier_point_1, bezier_point_2, resolution=20): - step = 1/resolution + step = 1 / resolution previous_p = bezier_point_1.co length = 0 for i in range(-1, resolution): @@ -164,28 +190,30 @@ def calc_bezier_length(bezier_point_1, bezier_point_2, resolution=20): previous_p = p return length -def calc_point_on_bezier_spline(bezier_spline_obj, - distance, - output_tangent = False, - resolution_factor = 1.0): + +def calc_point_on_bezier_spline( + bezier_spline_obj, distance, output_tangent=False, resolution_factor=1.0 +): # what's the point of just one point # assert len(bezier_spline_obj.bezier_points) >= 2 # 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") + print( + "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)) + return mathutils.Vector((0, 0, 0)), mathutils.Vector((1, 0, 0)) else: - return mathutils.Vector((0,0,0)) + return mathutils.Vector((0, 0, 0)) if len(bezier_spline_obj.bezier_points) == 1: p = bezier_spline_obj.bezier_points[0] travel = (p.handle_left - p.co).normalized() * distance if output_tangent: - tangent = mathutils.Vector((1,0,0)) + tangent = mathutils.Vector((1, 0, 0)) return travel, tangent else: return travel - + if distance <= 0: p = bezier_spline_obj.bezier_points[0] travel = (p.co - p.handle_left).normalized() * distance @@ -202,30 +230,30 @@ def calc_point_on_bezier_spline(bezier_spline_obj, total_length = 0 n_bezier_points = len(bezier_spline_obj.bezier_points) for i in range(0, len(bezier_spline_obj.bezier_points) - 1): - bezier = [ bezier_spline_obj.bezier_points[i], - bezier_spline_obj.bezier_points[i + 1] ] - length = calc_bezier_length(bezier[0], - bezier[1], - int(bezier_spline_obj.resolution_u * resolution_factor)) - total_length += length - beziers.append(bezier) - lengths.append(length) - # if total_length > distance: - # break + bezier = [ + bezier_spline_obj.bezier_points[i], + bezier_spline_obj.bezier_points[i + 1], + ] + length = calc_bezier_length( + bezier[0], + bezier[1], + int(bezier_spline_obj.resolution_u * resolution_factor), + ) + total_length += length + beziers.append(bezier) + lengths.append(length) + # if total_length > distance: + # break iterated_distance = 0 for i in range(0, len(beziers)): if iterated_distance + lengths[i] > distance: - distance_on_bezier = (distance - iterated_distance) + distance_on_bezier = distance - iterated_distance d = distance_on_bezier / lengths[i] # print(f"i: {i}, d: {d}, distance_on_bezier: {distance_on_bezier}, distance: {distance}") - location = calc_point_on_bezier(beziers[i][0], - beziers[i][1], - d) + location = calc_point_on_bezier(beziers[i][0], beziers[i][1], d) if output_tangent: - tangent = calc_tangent_on_bezier(beziers[i][0], - beziers[i][1], - d) + tangent = calc_tangent_on_bezier(beziers[i][0], beziers[i][1], d) return location, tangent else: return location @@ -236,19 +264,19 @@ def calc_point_on_bezier_spline(bezier_spline_obj, travel = (p.handle_right - p.co).normalized() * (distance - total_length) location = p.co + travel if output_tangent: - tangent = calc_tangent_on_bezier(beziers[last_i][0], - p, - 1) + tangent = calc_tangent_on_bezier(beziers[last_i][0], p, 1) return location, tangent else: return location -def calc_point_on_bezier_curve(bezier_curve_obj, - distance, - output_tangent = False, - output_spline_index = False, - resolution_factor = 1.0): +def calc_point_on_bezier_curve( + bezier_curve_obj, + distance, + output_tangent=False, + output_spline_index=False, + resolution_factor=1.0, +): curve = bezier_curve_obj.data # Loop through all splines in the curve @@ -260,36 +288,35 @@ def calc_point_on_bezier_curve(bezier_curve_obj, if output_spline_index and output_tangent: # return value from c_p_o_b_s is a tuple # so we need to append tuple + tuple - return calc_point_on_bezier_spline(spline, - (distance - total_length), - output_tangent, - resolution_factor) + (i,) + return calc_point_on_bezier_spline( + spline, (distance - total_length), output_tangent, resolution_factor + ) + (i,) if output_spline_index and not output_tangent: # return value from c_p_o_b_s is a location vector # so we need to append with a comma - return calc_point_on_bezier_spline(spline, - (distance - total_length), - output_tangent, - resolution_factor), i + return ( + calc_point_on_bezier_spline( + spline, + (distance - total_length), + output_tangent, + resolution_factor, + ), + i, + ) else: - return calc_point_on_bezier_spline(spline, - (distance - total_length), - output_tangent, - resolution_factor) + return calc_point_on_bezier_spline( + spline, (distance - total_length), output_tangent, resolution_factor + ) total_length += length # TODO: can this fail? # 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)] +# return [obj for obj in bpy.context.scene.objects if obj.name.startswith(startswith) and if obj.name.endswith(endswith)] -def find_objects_by_name( - objects, - equals="", - contains="", - startswith="", - endswith=""): + +def find_objects_by_name(objects, equals="", contains="", startswith="", endswith=""): # handle equals if equals != "": index = objects.find(equals) @@ -297,48 +324,55 @@ def find_objects_by_name( return [objects[index]] return [] # handle others is more permissive - return [obj for obj in objects if obj.name.startswith(startswith) and obj.name.endswith(endswith) and obj.name.find(contains) >= 0] + return [ + obj + for obj in objects + if obj.name.startswith(startswith) + and obj.name.endswith(endswith) + and obj.name.find(contains) >= 0 + ] + + +def find_objects_by_custom_property(objects, property_name="", property_value=""): + return [ + obj + for obj in objects + if property_name in obj and obj[property_name] == property_value + ] -def find_objects_by_custom_property( - objects, - property_name="", - property_value=""): - return [obj for obj in objects if property_name in obj and obj[property_name] == property_value] def turn_collection_hierarchy_into_path(obj): parent_collection = obj.users_collection[0] - parent_names = [] + parent_names = [] parent_names.append(parent_collection.name) get_parent_collection_names(parent_collection, parent_names) parent_names.reverse() - return '\\'.join(parent_names) + return "\\".join(parent_names) + def find_font_object(fontcollection, font_name): - fonts = find_objects_by_custom_property(fontcollection.objects, - "is_font", - True) + fonts = find_objects_by_custom_property(fontcollection.objects, "is_font", True) for font in fonts: if font["font_name"] == font_name and font.parent == None: return font return None + def find_font_face_object(font_obj, face_name): - faces = find_objects_by_custom_property(font_obj.children, - "is_face", - True) + faces = find_objects_by_custom_property(font_obj.children, "is_face", True) for face in faces: if face["face_name"] == face_name: return face return None + def move_in_fontcollection(obj, fontcollection, allow_duplicates=False): # parent nesting structure # the font object - font_obj = find_font_object(fontcollection, - obj["font_name"]) + font_obj = find_font_object(fontcollection, obj["font_name"]) if font_obj == None: font_obj = bpy.data.objects.new(obj["font_name"], None) - font_obj.empty_display_type = 'PLAIN_AXES' + font_obj.empty_display_type = "PLAIN_AXES" fontcollection.objects.link(font_obj) # ensure custom properties are set @@ -346,11 +380,10 @@ def move_in_fontcollection(obj, fontcollection, allow_duplicates=False): font_obj["is_font"] = True # the face object as a child of font object - face_obj = find_font_face_object(font_obj, - obj["face_name"]) + face_obj = find_font_face_object(font_obj, obj["face_name"]) if face_obj == None: face_obj = bpy.data.objects.new(obj["face_name"], None) - face_obj.empty_display_type = 'PLAIN_AXES' + face_obj.empty_display_type = "PLAIN_AXES" face_obj["is_face"] = True fontcollection.objects.link(face_obj) @@ -365,7 +398,7 @@ def move_in_fontcollection(obj, fontcollection, allow_duplicates=False): glyphs_objs = find_objects_by_name(face_obj.children, startswith="glyphs") if len(glyphs_objs) <= 0: glyphs_obj = bpy.data.objects.new("glyphs", None) - glyphs_obj.empty_display_type = 'PLAIN_AXES' + glyphs_obj.empty_display_type = "PLAIN_AXES" fontcollection.objects.link(glyphs_obj) glyphs_obj.parent = face_obj elif len(glyphs_objs) > 1: @@ -377,9 +410,11 @@ def move_in_fontcollection(obj, fontcollection, allow_duplicates=False): glyphs_obj["font_name"] = obj["font_name"] def get_hash(o): - return hash(tuple(tuple(v.co) for v in o.data.vertices )) + return hash(tuple(tuple(v.co) for v in o.data.vertices)) - for other_obj in find_objects_by_custom_property(glyphs_obj.children, "glyph", obj["glyph"]): + for other_obj in find_objects_by_custom_property( + glyphs_obj.children, "glyph", obj["glyph"] + ): if get_hash(other_obj) == get_hash(obj) and not allow_duplicates: return other_obj @@ -394,14 +429,16 @@ def move_in_fontcollection(obj, fontcollection, allow_duplicates=False): return obj + def bpy_to_abspath(blender_path): return os.path.realpath(bpy.path.abspath(blender_path)) + 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"] @@ -414,22 +451,28 @@ def register_font_from_filepath(filepath): 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) + 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"Font loading error", 'ERROR', f"Filepath({filepath}) is not a *.glb or *.gltf file") + ShowMessageBox( + f"Font loading error", + "ERROR", + f"Filepath({filepath}) is not a *.glb or *.gltf file", + ) return False 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) + 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: @@ -448,25 +491,19 @@ def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""): font_name = o["font_name"] face_name = o["face_name"] - glyph_obj = move_in_fontcollection( - o, - fontcollection) + glyph_obj = move_in_fontcollection(o, fontcollection) glyph_obj_pointer = bpy.types.PointerProperty(glyph_obj) if glyph_obj == o: del o[marker_property] - Font.add_glyph( - font_name, - face_name, - glyph_id, - glyph_obj_pointer) + Font.add_glyph(font_name, face_name, glyph_id, glyph_obj_pointer) 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}) + 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}) for mff in modified_font_faces: glyphs = [] @@ -494,6 +531,7 @@ def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""): # completely_delete_objects(remove_list) + def update_available_fonts(): abc3d_data = bpy.context.scene.abc3d_data @@ -509,26 +547,30 @@ def update_available_fonts(): f.face_name = face_name print(f"{__name__} added {font_name} {face_name}") + # def update_available_texts(): - # abc3d_data = bpy.context.scene.abc3d_data - # for o in bpy.context.scene.objects: - # if "linked_textobject" in o.keys(): - # i = o["linked_textobject"] - # found = False - # if len(abc3d_data.available_texts) > i: - # if abc3d_data.available_texts[i].glyphs +# abc3d_data = bpy.context.scene.abc3d_data +# for o in bpy.context.scene.objects: +# if "linked_textobject" in o.keys(): +# i = o["linked_textobject"] +# found = False +# if len(abc3d_data.available_texts) > i: +# if abc3d_data.available_texts[i].glyphs + def getPreferences(context): preferences = context.preferences - return preferences.addons['abc3d'].preferences + return preferences.addons["abc3d"].preferences + # clear available fonts def clear_available_fonts(): bpy.context.scene.abc3d_data.available_fonts.clear() + def load_installed_fonts(): preferences = getPreferences(bpy.context) - font_dir = os.path.join(preferences.assets_dir,"fonts") + font_dir = os.path.join(preferences.assets_dir, "fonts") if os.path.exists(font_dir): for file in os.listdir(font_dir): if file.endswith(".glb") or file.endswith(".gltf"): @@ -536,13 +578,14 @@ def load_installed_fonts(): # 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"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 = os.path.join(preferences.assets_dir,"fonts") + font_dir = os.path.join(preferences.assets_dir, "fonts") if os.path.exists(font_dir): for file in os.listdir(font_dir): if file.endswith(".glb") or file.endswith(".gltf"): @@ -550,13 +593,14 @@ def register_installed_fonts(): # 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"available font: {f.font_name} {f.face_name}") register_font_from_filepath(font_path) + message_memory = [] -def ShowMessageBox(title = "Message Box", icon = 'INFO', message="", prevent_repeat=False): +def ShowMessageBox(title="Message Box", icon="INFO", message="", prevent_repeat=False): """Show a simple message box taken from `Link here `_ @@ -567,10 +611,10 @@ def ShowMessageBox(title = "Message Box", icon = 'INFO', message="", prevent_rep :type icon: str :param message: lines of text to display, a.k.a. the message :type message: str or (str, str, ..) - + TIP: Check `Link blender icons `_ for icons you can use TIP: Or even better, check `Link this addons `_ to also see the icons. - + usage: .. code-block:: python myLines=("line 1","line 2","line 3") @@ -579,7 +623,7 @@ def ShowMessageBox(title = "Message Box", icon = 'INFO', message="", prevent_rep or: .. code-block:: python butils.ShowMessageBox(title="",message=("AAAAAH","NOOOOO"),icon=) - + """ global message_memory if prevent_repeat: @@ -588,18 +632,22 @@ def ShowMessageBox(title = "Message Box", icon = 'INFO', message="", prevent_rep print("PREVENT PREVENT") return message_memory.append([title, icon, message]) - myLines=message + myLines = message + def draw(self, context): if isinstance(myLines, str): self.layout.label(text=myLines) elif hasattr(myLines, "__iter__"): for n in myLines: self.layout.label(text=n) - bpy.context.window_manager.popup_menu(draw, title = title, icon = icon) + + bpy.context.window_manager.popup_menu(draw, title=title, icon=icon) + def simply_delete_objects(objs): completely_delete_objects(objs) + def completely_delete_objects(objs, recursive=True): for g in objs: if type(g) != type(None): @@ -617,33 +665,43 @@ def completely_delete_objects(objs, recursive=True): # not important pass + def is_mesh(o): return type(o.data) == bpy.types.Mesh + def is_metrics_object(o): if f"{utils.prefix()}_type" in o: - return o[f"{utils.prefix()}_type"] == 'metrics' - return (re.match(".*_metrics$", o.name) != None or re.match(".*_metrics.[\d]{3}$", o.name) != None) and is_mesh(o) + return o[f"{utils.prefix()}_type"] == "metrics" + return ( + re.match(".*_metrics$", o.name) != None + or re.match(".*_metrics.[\d]{3}$", o.name) != None + ) and is_mesh(o) + def is_text_object(o): if f"{utils.prefix()}_type" in o: - return o[f"{utils.prefix()}_type"] == 'textobject' + return o[f"{utils.prefix()}_type"] == "textobject" for t in bpy.context.scene.abc3d_data.available_texts: if o == t.text_object: return True return False + def is_glyph(o): if f"{utils.prefix()}_type" in o: - return o[f"{utils.prefix()}_type"] == 'glyph' + return o[f"{utils.prefix()}_type"] == "glyph" try: - return type(o.parent) is not type(None) \ - and "glyphs" in o.parent.name \ - and is_mesh(o) \ - and not is_metrics_object(o) + return ( + type(o.parent) is not type(None) + and "glyphs" in o.parent.name + and is_mesh(o) + and not is_metrics_object(o) + ) except ReferenceError as e: return False + def update_types(): scene = bpy.context.scene abc3d_data = scene.abc3d_data @@ -652,33 +710,36 @@ def update_types(): for g in t.glyphs: g.glyph_object[f"{utils.prefix()}_type"] = "glyph" + # blender bound_box vertices -# -# 3------7. +# +# 3------7. # |`. | `. +y # | `2------6 | # | | | | | # 0---|--4. | +--- +x -# `. | `.| `. +# `. | `.| `. # `1------5 `+z + def get_glyph_advance(glyph_obj): for c in glyph_obj.children: if is_metrics_object(c): 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_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, allow_replacement=True): loaded, missing, loadable, files = Font.test_glyphs_availability( - font_name, - face_name, - text) + font_name, face_name, text + ) # possibly replace upper and lower case letters with each other if len(missing) > 0 and allow_replacement: replacement_search = "" @@ -694,16 +755,18 @@ def prepare_text(font_name, face_name, text, allow_replacement=True): load_font_from_filepath(filepath, loadable, font_name, face_name) return True + def is_bezier(curve): - if curve.type != 'CURVE': + if curve.type != "CURVE": return False if len(curve.data.splines) < 1: return False for spline in curve.data.splines: - if spline.type != 'BEZIER': + if spline.type != "BEZIER": return False return True + def will_regenerate(text_properties): mom = text_properties.text_object @@ -713,7 +776,7 @@ def will_regenerate(text_properties): for i, g in enumerate(text_properties.glyphs): if not hasattr(g.glyph_object, "type"): return True - elif g.glyph_object.type != 'EMPTY': + elif g.glyph_object.type != "EMPTY": return True # check if perhaps one glyph was deleted elif type(g.glyph_object) == type(None): @@ -724,8 +787,11 @@ def will_regenerate(text_properties): return True elif len(text_properties.text) > i and g.glyph_id != text_properties.text[i]: return True - elif len(text_properties.text) > i and (g.glyph_object[f"{utils.prefix()}_font_name"] != text_properties.font_name - or g.glyph_object[f"{utils.prefix()}_face_name"] != text_properties.face_name): + elif len(text_properties.text) > i and ( + g.glyph_object[f"{utils.prefix()}_font_name"] != text_properties.font_name + or g.glyph_object[f"{utils.prefix()}_face_name"] + != text_properties.face_name + ): return True return False @@ -750,22 +816,22 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) if mom.type != "CURVE": return False - distribution_type = 'CALCULATE' if is_bezier(mom) else 'FOLLOW_PATH' + distribution_type = "CALCULATE" if is_bezier(mom) else "FOLLOW_PATH" # use_path messes with parenting # however, we need it for follow_path # https://projects.blender.org/blender/blender/issues/100661 previous_use_path = mom.data.use_path - if distribution_type == 'CALCULATE': + if distribution_type == "CALCULATE": mom.data.use_path = False - elif distribution_type == 'FOLLOW_PATH': + elif distribution_type == "FOLLOW_PATH": mom.data.use_path = True regenerate = will_regenerate(text_properties) # if we regenerate.... delete objects if regenerate and text_properties.get("glyphs"): - glyph_objects = [ g["glyph_object"] for g in text_properties["glyphs"] ] + glyph_objects = [g["glyph_object"] for g in text_properties["glyphs"]] completely_delete_objects(glyph_objects, True) text_properties.glyphs.clear() @@ -777,47 +843,53 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) for i, c in enumerate(text_properties.text): face = Font.fonts[text_properties.font_name].faces[text_properties.face_name] scalor = face.unit_factor * text_properties.font_size - if c == '\\': + if c == "\\": is_command = True continue is_newline = False if is_command: - if c == 'n': + if c == "n": is_newline = True next_line_advance = get_next_line_advance(mom, advance, glyph_advance) if advance == next_line_advance: # self.report({'INFO'}, f"would like to add new line for {text_properties.text} please") - print(f"would like to add new line for {text_properties.text} please") + print( + f"would like to add new line for {text_properties.text} please" + ) # TODO: add a new line advance = next_line_advance + text_properties.offset continue is_command = False glyph_id = c - glyph_tmp = Font.get_glyph(text_properties.font_name, - text_properties.face_name, - glyph_id) + glyph_tmp = Font.get_glyph( + text_properties.font_name, text_properties.face_name, glyph_id + ) if glyph_tmp == None: space_width = Font.is_space(glyph_id) if space_width != False: advance = advance + space_width * text_properties.font_size continue - message=f"Glyph not found for font_name='{text_properties.font_name}' face_name='{text_properties.face_name}' glyph_id='{glyph_id}'" + message = f"Glyph not found for font_name='{text_properties.font_name}' face_name='{text_properties.face_name}' glyph_id='{glyph_id}'" replaced = False if glyph_id.isalpha(): possible_replacement = glyph_id.swapcase() - glyph_tmp = Font.get_glyph(text_properties.font_name, - text_properties.face_name, - possible_replacement) + glyph_tmp = Font.get_glyph( + text_properties.font_name, + text_properties.face_name, + possible_replacement, + ) if glyph_tmp != None: message = message + f" (replaced with '{possible_replacement}')" replaced = True - - ShowMessageBox(title="Glyph replaced" if replaced else "Glyph missing", - icon='INFO' if replaced else 'ERROR', - message=message, - prevent_repeat=True) + + ShowMessageBox( + title="Glyph replaced" if replaced else "Glyph missing", + icon="INFO" if replaced else "ERROR", + message=message, + prevent_repeat=True, + ) if replaced == False: continue glyph = glyph_tmp.original @@ -837,8 +909,8 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) if c.name.startswith(f"{glyph_id}_mesh"): obg = c - if distribution_type == 'FOLLOW_PATH': - ob.constraints.new(type='FOLLOW_PATH') + if distribution_type == "FOLLOW_PATH": + ob.constraints.new(type="FOLLOW_PATH") ob.constraints["Follow Path"].target = mom ob.constraints["Follow Path"].use_fixed_location = True ob.constraints["Follow Path"].offset_factor = advance / curve_length @@ -846,29 +918,33 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) ob.constraints["Follow Path"].forward_axis = "FORWARD_X" ob.constraints["Follow Path"].up_axis = "UP_Y" spline_index = 0 - elif distribution_type == 'CALCULATE': + elif distribution_type == "CALCULATE": previous_ob_rotation_mode = None previous_obg_rotation_mode = None - if ob.rotation_mode != 'QUATERNION': - ob.rotation_mode = 'QUATERNION' + if ob.rotation_mode != "QUATERNION": + ob.rotation_mode = "QUATERNION" previous_ob_rotation_mode = ob.rotation_mode - if obg.rotation_mode != 'QUATERNION': - obg.rotation_mode = 'QUATERNION' + if obg.rotation_mode != "QUATERNION": + obg.rotation_mode = "QUATERNION" previous_obg_rotation_mode = obg.rotation_mode - location, tangent, spline_index = calc_point_on_bezier_curve(mom, advance, True, True) + location, tangent, spline_index = calc_point_on_bezier_curve( + mom, advance, True, True + ) if spline_index != previous_spline_index: is_newline = True if regenerate: - ob.location = mom.matrix_world @ (location + text_properties.translation) + ob.location = mom.matrix_world @ ( + location + text_properties.translation + ) mom.users_collection[0].objects.link(obg) mom.users_collection[0].objects.link(ob) ob.parent = mom obg.parent = ob obg.location = mathutils.Vector((0.0, 0.0, 0.0)) else: - ob.location = (location + text_properties.translation) + ob.location = location + text_properties.translation if not text_properties.ignore_orientation: mask = [0] @@ -876,24 +952,26 @@ 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) + motor = align_rotations_auto_pivot( + mask, input_rotations, vectors, factors, local_main_axis + ) q = mathutils.Quaternion() q.rotate(text_properties.orientation) if regenerate: obg.rotation_quaternion = q - ob.rotation_quaternion = (mom.matrix_world @ motor[0]).to_quaternion() + ob.rotation_quaternion = ( + mom.matrix_world @ motor[0] + ).to_quaternion() else: ob.rotation_quaternion = motor[0].to_quaternion() else: q = mathutils.Quaternion() q.rotate(text_properties.orientation) # obg.rotation_quaternion = q - obg.rotation_quaternion = (mom.matrix_world @ q.to_matrix().to_4x4()).to_quaternion() + obg.rotation_quaternion = ( + mom.matrix_world @ q.to_matrix().to_4x4() + ).to_quaternion() # ob.rotation_quaternion = (mom.matrix_world @ q.to_matrix().to_4x4()).to_quaternion() if previous_ob_rotation_mode: @@ -901,29 +979,45 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) if previous_obg_rotation_mode: obg.rotation_mode = previous_obg_rotation_mode - glyph_advance = get_glyph_advance(glyph) * scalor + text_properties.letter_spacing + glyph_advance = ( + get_glyph_advance(glyph) * scalor + text_properties.letter_spacing + ) # now we need to compensate for curvature # otherwise letters will be closer together the curvier the bezier is # this could be done more efficiently, but whatever curve_compensation = 0 - if distribution_type == 'CALCULATE' and (not is_newline or spline_index == 0): # TODO: fix newline hack + if distribution_type == "CALCULATE" and ( + not is_newline or spline_index == 0 + ): # TODO: fix newline hack if text_properties.compensate_curvature and glyph_advance > 0: - previous_location, psi = calc_point_on_bezier_curve(mom, advance, False, True) - new_location, si = calc_point_on_bezier_curve(mom, advance + glyph_advance, False, True) + previous_location, psi = calc_point_on_bezier_curve( + mom, advance, False, True + ) + new_location, si = calc_point_on_bezier_curve( + mom, advance + glyph_advance, False, True + ) if psi == si: - while (previous_location - new_location).length > glyph_advance and psi == si: + while ( + previous_location - new_location + ).length > glyph_advance and psi == si: curve_compensation = curve_compensation - glyph_advance * 0.01 - new_location, si = calc_point_on_bezier_curve(mom, - advance + glyph_advance + curve_compensation, - output_tangent=False, - output_spline_index=True) - while (previous_location - new_location).length < glyph_advance and psi == si: + new_location, si = calc_point_on_bezier_curve( + mom, + advance + glyph_advance + curve_compensation, + output_tangent=False, + output_spline_index=True, + ) + while ( + previous_location - new_location + ).length < glyph_advance and psi == si: curve_compensation = curve_compensation + glyph_advance * 0.01 - new_location, si = calc_point_on_bezier_curve(mom, - advance + glyph_advance + curve_compensation, - output_tangent=False, - output_spline_index=True) + new_location, si = calc_point_on_bezier_curve( + mom, + advance + glyph_advance + curve_compensation, + output_tangent=False, + output_spline_index=True, + ) ob.scale = (scalor, scalor, scalor) @@ -947,9 +1041,13 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) mom[f"{utils.prefix()}_translation"] = text_properties.translation if "lock_depsgraph_update_ntimes" in bpy.context.scene.abc3d_data: - bpy.context.scene.abc3d_data["lock_depsgraph_update_ntimes"] += len(bpy.context.selected_objects) + bpy.context.scene.abc3d_data["lock_depsgraph_update_ntimes"] += len( + bpy.context.selected_objects + ) else: - bpy.context.scene.abc3d_data["lock_depsgraph_update_ntimes"] = len(bpy.context.selected_objects) + bpy.context.scene.abc3d_data["lock_depsgraph_update_ntimes"] = len( + bpy.context.selected_objects + ) # NOTE: we reset with a timeout, as setting and resetting certain things # in fast succession will cause visual glitches (e.g. {}.data.use_path). @@ -961,6 +1059,7 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) bpy.app.timers.unregister(reset) molotov = reset_depsgraph_n + 0 + def counted_reset(scene, depsgraph): nonlocal molotov if molotov == 0: @@ -987,20 +1086,23 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) return True + verification_object = { - f"{utils.prefix()}_type": "textobject", - f"{utils.prefix()}_linked_textobject": 0, - f"{utils.prefix()}_font_name": "font_name", - f"{utils.prefix()}_face_name": "face_name", - f"{utils.prefix()}_font_size": 42, - f"{utils.prefix()}_letter_spacing": 42, - f"{utils.prefix()}_orientation": [0,0,0], - f"{utils.prefix()}_translation": [0,0,0], - } + f"{utils.prefix()}_type": "textobject", + f"{utils.prefix()}_linked_textobject": 0, + f"{utils.prefix()}_font_name": "font_name", + f"{utils.prefix()}_face_name": "face_name", + f"{utils.prefix()}_font_size": 42, + f"{utils.prefix()}_letter_spacing": 42, + f"{utils.prefix()}_orientation": [0, 0, 0], + f"{utils.prefix()}_translation": [0, 0, 0], +} + def verify_text_object(o): pass + def transfer_text_properties_to_text_object(text_properties, o): o[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id o[f"{utils.prefix()}_font_name"] = text_properties.font_name @@ -1011,6 +1113,7 @@ def transfer_text_properties_to_text_object(text_properties, o): o[f"{utils.prefix()}_translation"] = text_properties.translation o[f"{utils.prefix()}_text"] = text_properties["text"] + def transfer_text_object_to_text_properties(o, text_properties): text_properties["text_id"] = o[f"{utils.prefix()}_linked_textobject"] text_properties["font_name"] = o[f"{utils.prefix()}_font_name"] @@ -1021,15 +1124,17 @@ def transfer_text_object_to_text_properties(o, text_properties): text_properties["translation"] = o[f"{utils.prefix()}_translation"] text_properties["text"] = o[f"{utils.prefix()}_text"] + # blender bound_box vertices -# -# 3------7. +# +# 3------7. # |`. | `. +y # | `2------6 -z | # | | | | `. | # 0---|--4. | `+--- +x -# `. | `.| -# `1------5 +# `. | `.| +# `1------5 + def add_metrics_obj_from_bound_box(glyph, bound_box=None): mesh = bpy.data.meshes.new(f"{glyph.name}_metrics") # add the new mesh @@ -1054,49 +1159,78 @@ def add_metrics_obj_from_bound_box(glyph, bound_box=None): if type(bound_box) == type(None): bound_box = glyph.bound_box - - verts = [bound_box[0], - bound_box[1], - bound_box[2], - bound_box[3], - bound_box[4], - bound_box[5], - bound_box[6], - bound_box[7], - ] - edges = [[0,1],[1,2],[2,3],[3,0], - [0,4],[1,5],[2,6],[3,7], - [4,5],[5,6],[6,7],[7,4], - ] + + verts = [ + bound_box[0], + bound_box[1], + bound_box[2], + bound_box[3], + bound_box[4], + bound_box[5], + bound_box[6], + bound_box[7], + ] + edges = [ + [0, 1], + [1, 2], + [2, 3], + [3, 0], + [0, 4], + [1, 5], + [2, 6], + [3, 7], + [4, 5], + [5, 6], + [6, 7], + [7, 4], + ] faces = [] - + mesh.from_pydata(verts, edges, faces) + def add_faces_to_metrics(obj): mesh = bpy.data.meshes.new(f"{obj.name}") # add the new mesh print(f"add_faces_to_metrics for {obj.name}") bound_box = bound_box_as_array(obj.bound_box) - verts = [bound_box[0], - bound_box[1], - bound_box[2], - bound_box[3], - bound_box[4], - bound_box[5], - bound_box[6], - bound_box[7], - ] - edges = [[0,1],[1,2],[2,3],[3,0], - [0,4],[1,5],[2,6],[3,7], - [4,5],[5,6],[6,7],[7,4], - ] + verts = [ + bound_box[0], + bound_box[1], + bound_box[2], + bound_box[3], + bound_box[4], + bound_box[5], + bound_box[6], + bound_box[7], + ] + edges = [ + [0, 1], + [1, 2], + [2, 3], + [3, 0], + [0, 4], + [1, 5], + [2, 6], + [3, 7], + [4, 5], + [5, 6], + [6, 7], + [7, 4], + ] faces = [ - [0,1,2], [2,3,0], - [2,6,7], [7,3,2], - [6,5,4], [4,7,6], - [4,5,1], [0,4,1], - [1,5,6], [1,6,2], - [4,0,7], [7,0,3], - ] + [0, 1, 2], + [2, 3, 0], + [2, 6, 7], + [7, 3, 2], + [6, 5, 4], + [4, 7, 6], + [4, 5, 1], + [0, 4, 1], + [1, 5, 6], + [1, 6, 2], + [4, 0, 7], + [7, 0, 3], + ] mesh.from_pydata(verts, edges, faces) @@ -1104,24 +1238,35 @@ def add_faces_to_metrics(obj): obj.data = mesh bpy.data.meshes.remove(old_mesh) + def remove_faces_from_metrics(obj): mesh = bpy.data.meshes.new(f"{obj.name}") # add the new mesh bound_box = bound_box_as_array(obj.bound_box) - verts = [bound_box[0], - bound_box[1], - bound_box[2], - bound_box[3], - bound_box[4], - bound_box[5], - bound_box[6], - bound_box[7], - ] - edges = [[0,1],[1,2],[2,3],[3,0], - [0,4],[1,5],[2,6],[3,7], - [4,5],[5,6],[6,7],[7,4], - ] - faces = [ - ] + verts = [ + bound_box[0], + bound_box[1], + bound_box[2], + bound_box[3], + bound_box[4], + bound_box[5], + bound_box[6], + bound_box[7], + ] + edges = [ + [0, 1], + [1, 2], + [2, 3], + [3, 0], + [0, 4], + [1, 5], + [2, 6], + [3, 7], + [4, 5], + [5, 6], + [6, 7], + [7, 4], + ] + faces = [] mesh.from_pydata(verts, edges, faces) @@ -1129,59 +1274,63 @@ def remove_faces_from_metrics(obj): obj.data = mesh bpy.data.meshes.remove(old_mesh) + # duplicate # def remove_metrics_from_selection(): - # for o in bpy.context.selected_objects: - # is_possibly_glyph = is_mesh(o) - # if is_possibly_glyph: - # metrics = [] - # for c in o.children: - # if is_metrics_object(c): - # metrics.append(c) - # completely_delete_objects(metrics) +# for o in bpy.context.selected_objects: +# is_possibly_glyph = is_mesh(o) +# if is_possibly_glyph: +# metrics = [] +# for c in o.children: +# if is_metrics_object(c): +# metrics.append(c) +# completely_delete_objects(metrics) -def get_max_bound_box(bb_1, bb_2 = None): + +def get_max_bound_box(bb_1, bb_2=None): if type(bb_2) == type(None): bb_2 = bb_1 x_max = max(bb_1[4][0], bb_2[4][0]) x_min = min(bb_1[0][0], bb_2[0][0]) - y_max = max(bb_1[3][1], bb_2[3][1]) - y_min = min(bb_1[0][1], bb_2[0][1]) + y_max = max(bb_1[3][1], bb_2[3][1]) + y_min = min(bb_1[0][1], bb_2[0][1]) z_max = max(bb_1[1][2], bb_2[1][2]) z_min = min(bb_1[0][2], bb_2[0][2]) return [ - mathutils.Vector((x_min, y_min, z_min)), - mathutils.Vector((x_min, y_min, z_max)), - mathutils.Vector((x_min, y_max, z_max)), - mathutils.Vector((x_min, y_max, z_min)), + mathutils.Vector((x_min, y_min, z_min)), + mathutils.Vector((x_min, y_min, z_max)), + mathutils.Vector((x_min, y_max, z_max)), + mathutils.Vector((x_min, y_max, z_min)), + mathutils.Vector((x_max, y_min, z_min)), + mathutils.Vector((x_max, y_min, z_max)), + mathutils.Vector((x_max, y_max, z_max)), + mathutils.Vector((x_max, y_max, z_min)), + ] - mathutils.Vector((x_max, y_min, z_min)), - mathutils.Vector((x_max, y_min, z_max)), - mathutils.Vector((x_max, y_max, z_max)), - mathutils.Vector((x_max, y_max, z_min)), - ] # blender bound_box vertices -# -# 3------7. +# +# 3------7. # |`. | `. +y # | `2------6 | # | | | | | # 0---|--4. | +--- +x -# `. | `.| `. +# `. | `.| `. # `1------5 `+z + # why not [ [0] * 3 ] * 8 # https://stackoverflow.com/questions/2397141/how-to-initialize-a-two-dimensional-array-list-of-lists-if-not-using-numpy-in def bound_box_as_array(bound_box): - array = [ [0] * 3 for i in range(8) ] + array = [[0] * 3 for i in range(8)] for i in range(0, len(bound_box)): for j in range(0, len(bound_box[i])): array[i][j] = bound_box[i][j] return array + ## -# @brief get_metrics_bound_box +# @brief get_metrics_bound_box # generates a metrics bounding box # where x-width comes from bb # and y-height + z-depth from bb_uebermetrics @@ -1207,6 +1356,7 @@ 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: @@ -1214,15 +1364,17 @@ def get_metrics_object(o): return c return None + def get_original(o): if hasattr(o, "original"): return o.original else: return o + def add_default_metrics_to_objects(objects=None, overwrite_existing=False): if type(objects) == type(None): - objects=bpy.context.selected_objects + objects = bpy.context.selected_objects targets = [] reference_bound_box = None for o in objects: @@ -1235,21 +1387,28 @@ def add_default_metrics_to_objects(objects=None, overwrite_existing=False): if len(metrics) == 0: targets.append(o) - reference_bound_box = get_max_bound_box(o.bound_box, reference_bound_box) + reference_bound_box = get_max_bound_box( + o.bound_box, reference_bound_box + ) elif len(metrics) >= 0 and overwrite_existing: completely_delete_objects(metrics) targets.append(o) - reference_bound_box = get_max_bound_box(o.bound_box, reference_bound_box) + reference_bound_box = get_max_bound_box( + o.bound_box, reference_bound_box + ) else: for m in metrics: - reference_bound_box = get_max_bound_box(m.bound_box, reference_bound_box) + reference_bound_box = get_max_bound_box( + m.bound_box, reference_bound_box + ) for t in targets: bound_box = get_metrics_bound_box(t.bound_box, reference_bound_box) add_metrics_obj_from_bound_box(t, bound_box) + def remove_metrics_from_objects(objects=None): if type(objects) == type(None): - objects=bpy.context.selected_objects + objects = bpy.context.selected_objects metrics = [] for o in objects: for c in o.children: @@ -1257,9 +1416,10 @@ def remove_metrics_from_objects(objects=None): metrics.append(c) completely_delete_objects(metrics) + def align_metrics_of_objects_to_active_object(objects=None): if type(objects) == type(None): - objects=bpy.context.selected_objects + objects = bpy.context.selected_objects if len(objects) == 0: return "no objects selected" @@ -1279,7 +1439,7 @@ def align_metrics_of_objects_to_active_object(objects=None): # do it for o in objects: is_possibly_glyph = is_glyph(o) - if is_possibly_glyph: + if is_possibly_glyph and not o is bpy.context.active_object: metrics = [] for c in o.children: if is_metrics_object(c): @@ -1287,20 +1447,19 @@ def align_metrics_of_objects_to_active_object(objects=None): bb = None if len(metrics) == 0: - bb = get_metrics_bound_box(o.bound_box, - reference_bound_box) + bb = get_metrics_bound_box(o.bound_box, reference_bound_box) else: - bb = get_metrics_bound_box(metrics[0].bound_box, - reference_bound_box) + bb = get_metrics_bound_box(metrics[0].bound_box, reference_bound_box) if len(metrics) > 0: completely_delete_objects(metrics) - + add_metrics_obj_from_bound_box(o, bb) return "" + def align_metrics_of_objects(objects=None): if type(objects) == type(None): - objects=bpy.context.selected_objects + objects = bpy.context.selected_objects if len(objects) == 0: return "no objects selected" @@ -1315,11 +1474,13 @@ def align_metrics_of_objects(objects=None): metrics.append(c) if len(metrics) == 0: - reference_bound_box = get_max_bound_box(o.bound_box, - reference_bound_box) + reference_bound_box = get_max_bound_box( + o.bound_box, reference_bound_box + ) elif len(metrics) > 0: - reference_bound_box = get_max_bound_box(metrics[0].bound_box, - reference_bound_box) + reference_bound_box = get_max_bound_box( + metrics[0].bound_box, reference_bound_box + ) targets.append(o) for t in targets: metrics = [] @@ -1328,13 +1489,10 @@ def align_metrics_of_objects(objects=None): metrics.append(c) bound_box = None if len(metrics) == 0: - bound_box = get_metrics_bound_box(t.bound_box, - reference_bound_box) + bound_box = get_metrics_bound_box(t.bound_box, reference_bound_box) else: - bound_box = get_metrics_bound_box(metrics[0].bound_box, - reference_bound_box) + bound_box = get_metrics_bound_box(metrics[0].bound_box, reference_bound_box) completely_delete_objects(metrics) - + add_metrics_obj_from_bound_box(t, bound_box) return "" - diff --git a/common/utils.py b/common/utils.py index d1eab92..0c99dfd 100644 --- a/common/utils.py +++ b/common/utils.py @@ -1,25 +1,33 @@ # NOTE: also change version in ../__init__.py def get_version_major(): return 0 + + def get_version_minor(): return 0 + + def get_version_patch(): return 5 + + def get_version_string(): return f"{get_version_major()}.{get_version_minor()}.{get_version_patch}" + + def prefix(): return "ABC3D" -import time + import datetime -from mathutils import ( - Vector, - ) +import time + +from mathutils import Vector + def get_timestamp(): - return datetime.datetime \ - .fromtimestamp(time.time()) \ - .strftime('%Y.%m.%d-%H:%M:%S') + return datetime.datetime.fromtimestamp(time.time()).strftime("%Y.%m.%d-%H:%M:%S") + def mapRange(in_value, in_min, in_max, out_min, out_max, clamp=False): output = out_min + ((out_max - out_min) / (in_max - in_min)) * (in_value - in_min) @@ -32,36 +40,43 @@ def mapRange(in_value, in_min, in_max, out_min, out_max, clamp=False): return output -import warnings import functools +import warnings + def deprecated(func): """This is a decorator which can be used to mark functions as deprecated. It will result in a warning being emitted when the function is used.""" + @functools.wraps(func) def new_func(*args, **kwargs): - warnings.simplefilter('always', DeprecationWarning) # turn off filter - warnings.warn("Call to deprecated function {}.".format(func.__name__), - category=DeprecationWarning, - stacklevel=2) - warnings.simplefilter('default', DeprecationWarning) # reset filter + warnings.simplefilter("always", DeprecationWarning) # turn off filter + warnings.warn( + "Call to deprecated function {}.".format(func.__name__), + category=DeprecationWarning, + stacklevel=2, + ) + warnings.simplefilter("default", DeprecationWarning) # reset filter return func(*args, **kwargs) + return new_func import subprocess import sys + + def open_file_browser(directory): - if sys.platform=='win32': + if sys.platform == "win32": os.startfile(directory) - - elif sys.platform=='darwin': - subprocess.Popen(['open', directory]) - + + elif sys.platform == "darwin": + subprocess.Popen(["open", directory]) + else: try: - subprocess.Popen(['xdg-open', directory]) + subprocess.Popen(["xdg-open", directory]) except OSError: pass # er, think of something else to try @@ -73,28 +88,28 @@ def printerr(*args, **kwargs): def removeNonAlphabetic(s): - return ''.join([i for i in s if i.isalpha()]) + return "".join([i for i in s if i.isalpha()]) # # Evaluate a bezier curve for the parameter 0<=t<=1 along its length # def evaluateBezierPoint(p1, h1, h2, p2, t): - # return ((1 - t)**3) * p1 + (3 * t * (1 - t)**2) * h1 + (3 * (t**2) * (1 - t)) * h2 + (t**3) * p2 +# return ((1 - t)**3) * p1 + (3 * t * (1 - t)**2) * h1 + (3 * (t**2) * (1 - t)) * h2 + (t**3) * p2 # # Evaluate the unit tangent on a bezier curve for t # def evaluateBezierTangent(p1, h1, h2, p2, t): - # return ( - # (-3 * (1 - t)**2) * p1 + (-6 * t * (1 - t) + 3 * (1 - t)**2) * h1 + - # (-3 * (t**2) + 6 * t * (1 - t)) * h2 + (3 * t**2) * p2 - # ).normalized() +# return ( +# (-3 * (1 - t)**2) * p1 + (-6 * t * (1 - t) + 3 * (1 - t)**2) * h1 + +# (-3 * (t**2) + 6 * t * (1 - t)) * h2 + (3 * t**2) * p2 +# ).normalized() # def calculateBezierLength(p1, h1, h2, p2, resolution=20): - # step = 1/resolution - # previous_p = p1 - # length = 0 - # for i in range(0, resolution): - # t = (i + 1) * step - # p = evaluateBezierPoint(p1, h1, h2, p2, t) - # length += p.distance(previous_p) - # previous_p = p - # return length +# step = 1/resolution +# previous_p = p1 +# length = 0 +# for i in range(0, resolution): +# t = (i + 1) * step +# p = evaluateBezierPoint(p1, h1, h2, p2, t) +# length += p.distance(previous_p) +# previous_p = p +# return length From a6812450938f096c2ccb1cef3dc1f6d2b9f46337 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Tue, 13 May 2025 16:08:40 +0200 Subject: [PATCH 030/103] more explanation --- __init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/__init__.py b/__init__.py index ad95aba..13acac9 100644 --- a/__init__.py +++ b/__init__.py @@ -1139,7 +1139,9 @@ class ABC3D_OT_PlaceText(bpy.types.Operator): class ABC3D_OT_ToggleABC3DCollection(bpy.types.Operator): - """Toggle ABC3D Collection""" + """Toggle ABC3D Collection. + + 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" From ff85c935512178c5593f2cd36f4eccd217e1e870 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Tue, 13 May 2025 16:08:53 +0200 Subject: [PATCH 031/103] formatting --- __init__.py | 25 ++- addon_updater.py | 312 ++++++++++++++++--------------- addon_updater_ops.py | 430 ++++++++++++++++++++++--------------------- bimport.py | 246 +++++++++++++++---------- common/Font.py | 177 ++++++++++-------- 5 files changed, 641 insertions(+), 549 deletions(-) diff --git a/__init__.py b/__init__.py index 13acac9..f8c2e0a 100644 --- a/__init__.py +++ b/__init__.py @@ -150,7 +150,6 @@ class ABC3D_glyph_properties(bpy.types.PropertyGroup): class ABC3D_text_properties(bpy.types.PropertyGroup): - def font_items_callback(self, context): items = [] for f in Font.get_loaded_fonts_and_faces(): @@ -291,7 +290,7 @@ class ABC3D_data(bpy.types.PropertyGroup): ) export_dir: bpy.props.StringProperty( name="Export Directory", - description=f"The directory in which we will export fonts.\nIf it is blank, we will export to the addon assets path.\nThis is where the fonts are installed.", + description="The directory in which we will export fonts.\nIf it is blank, we will export to the addon assets path.\nThis is where the fonts are installed.", subtype="DIR_PATH", ) @@ -381,7 +380,7 @@ class ABC3D_PT_FontList(bpy.types.Panel): box.row().label(text=f"Face Name: {face_name}") n = 16 n_rows = int(len(available_glyphs) / n) - box.row().label(text=f"Glyphs:") + box.row().label(text="Glyphs:") subbox = box.box() for i in range(0, n_rows + 1): text = "".join( @@ -397,7 +396,7 @@ class ABC3D_PT_FontList(bpy.types.Panel): row.alignment = "CENTER" row.label(text=text) n_rows = int(len(loaded_glyphs) / n) - box.row().label(text=f"Loaded/Used Glyphs:") + box.row().label(text="Loaded/Used Glyphs:") subbox = box.box() for i in range(0, n_rows + 1): text = "".join( @@ -595,7 +594,9 @@ class ABC3D_PT_FontCreation(bpy.types.Panel): layout.row().operator( f"{__name__}.create_font_from_objects", text="Create/Extend Font" ) - layout.row().operator(f"{__name__}.save_font_to_file", text="Export Font To File") + layout.row().operator( + f"{__name__}.save_font_to_file", text="Export Font To File" + ) box = layout.box() box.label(text="metrics") box.row().operator( @@ -762,9 +763,9 @@ class ABC3D_OT_InstallFont(bpy.types.Operator): title=f"{__name__} Warning", icon="ERROR", message=[ - f"Could not install font.", + "Could not install font.", f"We believe the font path ({font_path}) does not exist.", - f"If this is an error, please let us know.", + "If this is an error, please let us know.", ], ) return {"CANCELLED"} @@ -1200,7 +1201,7 @@ class ABC3D_OT_SaveFontToFile(bpy.types.Operator): n = 16 n_rows = int(len(loaded_glyphs) / n) box = layout.box() - box.row().label(text=f"Glyphs to be exported:") + box.row().label(text="Glyphs to be exported:") subbox = box.box() for i in range(0, n_rows + 1): text = "".join( @@ -1311,7 +1312,7 @@ class ABC3D_OT_SaveFontToFile(bpy.types.Operator): butils.remove_faces_from_metrics(obj) bpy.app.timers.register(lambda: remove_faces(), first_interval=2) - self.report({"INFO"}, f"did it") + self.report({"INFO"}, "did it") return {"FINISHED"} @@ -1365,9 +1366,7 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): row = layout.row() row.prop(self, "autodetect_names") first_object_name = context.selected_objects[-1].name - 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() @@ -1663,7 +1662,7 @@ def on_depsgraph_update(scene, depsgraph): def later(): if ( - not "lock_depsgraph_update_ntimes" in scene.abc3d_data + "lock_depsgraph_update_ntimes" not in scene.abc3d_data or scene.abc3d_data["lock_depsgraph_update_ntimes"] <= 0 ): butils.set_text_on_curve( diff --git a/addon_updater.py b/addon_updater.py index 3ca5a3e..fed01ef 100644 --- a/addon_updater.py +++ b/addon_updater.py @@ -54,8 +54,8 @@ class SingletonUpdater: needed throughout the addon. It implements all the interfaces for running updates. """ - def __init__(self): + def __init__(self): self._engine = ForgejoEngine() self._user = None self._repo = None @@ -68,7 +68,7 @@ class SingletonUpdater: self._latest_release = None self._use_releases = False self._include_branches = False - self._include_branch_list = ['master'] + self._include_branch_list = ["master"] self._include_branch_auto_check = False self._manual_only = False self._version_min_update = None @@ -110,7 +110,8 @@ class SingletonUpdater: self._addon = __package__.lower() self._addon_package = __package__ # Must not change. self._updater_path = os.path.join( - os.path.dirname(__file__), self._addon + "_updater") + os.path.dirname(__file__), self._addon + "_updater" + ) self._addon_root = os.path.dirname(__file__) self._json = dict() self._error = None @@ -202,11 +203,13 @@ class SingletonUpdater: @property def check_interval(self): - return (self._check_interval_enabled, - self._check_interval_months, - self._check_interval_days, - self._check_interval_hours, - self._check_interval_minutes) + return ( + self._check_interval_enabled, + self._check_interval_months, + self._check_interval_days, + self._check_interval_hours, + self._check_interval_minutes, + ) @property def current_version(self): @@ -221,12 +224,10 @@ class SingletonUpdater: try: tuple(tuple_values) except: - raise ValueError( - "current_version must be a tuple of integers") + raise ValueError("current_version must be a tuple of integers") for i in tuple_values: if type(i) is not int: - raise ValueError( - "current_version must be a tuple of integers") + raise ValueError("current_version must be a tuple of integers") self._current_version = tuple(tuple_values) @property @@ -285,15 +286,15 @@ class SingletonUpdater: def include_branch_list(self, value): try: if value is None: - self._include_branch_list = ['master'] + self._include_branch_list = ["master"] elif not isinstance(value, list) or len(value) == 0: raise ValueError( - "include_branch_list should be a list of valid branches") + "include_branch_list should be a list of valid branches" + ) else: self._include_branch_list = value except: - raise ValueError( - "include_branch_list should be a list of valid branches") + raise ValueError("include_branch_list should be a list of valid branches") @property def include_branches(self): @@ -362,8 +363,7 @@ class SingletonUpdater: if value is None: self._remove_pre_update_patterns = list() elif not isinstance(value, list): - raise ValueError( - "remove_pre_update_patterns needs to be in a list format") + raise ValueError("remove_pre_update_patterns needs to be in a list format") else: self._remove_pre_update_patterns = value @@ -548,8 +548,7 @@ class SingletonUpdater: tag_names.append(tag["name"]) return tag_names - def set_check_interval(self, enabled=False, - months=0, days=14, hours=0, minutes=0): + def set_check_interval(self, enabled=False, months=0, days=14, hours=0, minutes=0): """Set the time interval between automated checks, and if enabled. Has enabled = False as default to not check against frequency, @@ -582,7 +581,8 @@ class SingletonUpdater: def __str__(self): return "Updater, with user: {a}, repository: {b}, url: {c}".format( - a=self._user, b=self._repo, c=self.form_repo_url()) + a=self._user, b=self._repo, c=self.form_repo_url() + ) # ------------------------------------------------------------------------- # API-related functions @@ -621,10 +621,7 @@ class SingletonUpdater: temp_branches.reverse() for branch in temp_branches: request = self.form_branch_url(branch) - include = { - "name": branch.title(), - "zipball_url": request - } + include = {"name": branch.title(), "zipball_url": request} self._tags = [include] + self._tags # append to front if self._tags is None: @@ -643,13 +640,18 @@ class SingletonUpdater: if not self._error: self._tag_latest = self._tags[0] branch = self._include_branch_list[0] - self.print_verbose("{} branch found, no releases: {}".format( - branch, self._tags[0])) + self.print_verbose( + "{} branch found, no releases: {}".format(branch, self._tags[0]) + ) - elif ((len(self._tags) - len(self._include_branch_list) == 0 - and self._include_branches) - or (len(self._tags) == 0 and not self._include_branches) - and self._prefiltered_tag_count > 0): + elif ( + ( + len(self._tags) - len(self._include_branch_list) == 0 + and self._include_branches + ) + or (len(self._tags) == 0 and not self._include_branches) + and self._prefiltered_tag_count > 0 + ): self._tag_latest = None self._error = "No releases available" self._error_msg = "No versions found within compatible version range" @@ -659,13 +661,15 @@ class SingletonUpdater: if not self._include_branches: self._tag_latest = self._tags[0] self.print_verbose( - "Most recent tag found:" + str(self._tags[0]['name'])) + "Most recent tag found:" + str(self._tags[0]["name"]) + ) else: # Don't return branch if in list. n = len(self._include_branch_list) self._tag_latest = self._tags[n] # guaranteed at least len()=n+1 self.print_verbose( - "Most recent tag found:" + str(self._tags[n]['name'])) + "Most recent tag found:" + str(self._tags[n]["name"]) + ) def get_raw(self, url): """All API calls to base url.""" @@ -680,13 +684,12 @@ class SingletonUpdater: # Setup private request headers if appropriate. if self._engine.token is not None: if self._engine.name == "gitlab": - request.add_header('PRIVATE-TOKEN', self._engine.token) + request.add_header("PRIVATE-TOKEN", self._engine.token) else: self.print_verbose("Tokens not setup for engine yet") # Always set user agent. - request.add_header( - 'User-Agent', "Python/" + str(platform.python_version())) + request.add_header("User-Agent", "Python/" + str(platform.python_version())) # Run the request. try: @@ -747,8 +750,7 @@ class SingletonUpdater: error = None # Make/clear the staging folder, to ensure the folder is always clean. - self.print_verbose( - "Preparing staging folder for download:\n" + str(local)) + self.print_verbose("Preparing staging folder for download:\n" + str(local)) if os.path.isdir(local): try: shutil.rmtree(local) @@ -782,17 +784,16 @@ class SingletonUpdater: # Setup private token if appropriate. if self._engine.token is not None: if self._engine.name == "gitlab": - request.add_header('PRIVATE-TOKEN', self._engine.token) + request.add_header("PRIVATE-TOKEN", self._engine.token) else: - self.print_verbose( - "Tokens not setup for selected engine yet") + self.print_verbose("Tokens not setup for selected engine yet") # Always set user agent - request.add_header( - 'User-Agent', "Python/" + str(platform.python_version())) + request.add_header("User-Agent", "Python/" + str(platform.python_version())) - self.url_retrieve(urllib.request.urlopen(request, context=context), - self._source_zip) + self.url_retrieve( + urllib.request.urlopen(request, context=context), self._source_zip + ) # Add additional checks on file size being non-zero. self.print_verbose("Successfully downloaded update zip") return True @@ -809,7 +810,8 @@ class SingletonUpdater: self.print_verbose("Backing up current addon folder") local = os.path.join(self._updater_path, "backup") tempdest = os.path.join( - self._addon_root, os.pardir, self._addon + "_updater_backup_temp") + self._addon_root, os.pardir, self._addon + "_updater_backup_temp" + ) self.print_verbose("Backup destination path: " + str(local)) @@ -818,7 +820,8 @@ class SingletonUpdater: shutil.rmtree(local) except: self.print_verbose( - "Failed to removed previous backup folder, continuing") + "Failed to removed previous backup folder, continuing" + ) self.print_trace() # Remove the temp folder. @@ -827,16 +830,17 @@ class SingletonUpdater: try: shutil.rmtree(tempdest) except: - self.print_verbose( - "Failed to remove existing temp folder, continuing") + self.print_verbose("Failed to remove existing temp folder, continuing") self.print_trace() # Make a full addon copy, temporarily placed outside the addon folder. if self._backup_ignore_patterns is not None: try: - shutil.copytree(self._addon_root, tempdest, - ignore=shutil.ignore_patterns( - *self._backup_ignore_patterns)) + shutil.copytree( + self._addon_root, + tempdest, + ignore=shutil.ignore_patterns(*self._backup_ignore_patterns), + ) except: print("Failed to create backup, still attempting update.") self.print_trace() @@ -853,7 +857,8 @@ class SingletonUpdater: # Save the date for future reference. now = datetime.now() self._json["backup_date"] = "{m}-{d}-{yr}".format( - m=now.strftime("%B"), d=now.day, yr=now.year) + m=now.strftime("%B"), d=now.day, yr=now.year + ) self.save_updater_json() def restore_backup(self): @@ -861,7 +866,8 @@ class SingletonUpdater: self.print_verbose("Restoring backup, backing up current addon folder") backuploc = os.path.join(self._updater_path, "backup") tempdest = os.path.join( - self._addon_root, os.pardir, self._addon + "_updater_backup_temp") + self._addon_root, os.pardir, self._addon + "_updater_backup_temp" + ) tempdest = os.path.abspath(tempdest) # Move instead contents back in place, instead of copy. @@ -910,10 +916,8 @@ class SingletonUpdater: self._error_msg = "Failed to create extract directory" return -1 - self.print_verbose( - "Begin extracting source from zip:" + str(self._source_zip)) + self.print_verbose("Begin extracting source from zip:" + str(self._source_zip)) with zipfile.ZipFile(self._source_zip, "r") as zfile: - if not zfile: self._error = "Install failed" self._error_msg = "Resulting file is not a zip, cannot extract" @@ -923,19 +927,20 @@ class SingletonUpdater: # Now extract directly from the first subfolder (not root) # this avoids adding the first subfolder to the path length, # which can be too long if the download has the SHA in the name. - zsep = '/' # Not using os.sep, always the / value even on windows. + zsep = "/" # Not using os.sep, always the / value even on windows. for name in zfile.namelist(): if zsep not in name: continue - top_folder = name[:name.index(zsep) + 1] + top_folder = name[: name.index(zsep) + 1] if name == top_folder + zsep: continue # skip top level folder - sub_path = name[name.index(zsep) + 1:] + sub_path = name[name.index(zsep) + 1 :] if name.endswith(zsep): try: os.mkdir(os.path.join(outdir, sub_path)) self.print_verbose( - "Extract - mkdir: " + os.path.join(outdir, sub_path)) + "Extract - mkdir: " + os.path.join(outdir, sub_path) + ) except OSError as exc: if exc.errno != errno.EEXIST: self._error = "Install failed" @@ -947,7 +952,8 @@ class SingletonUpdater: data = zfile.read(name) outfile.write(data) self.print_verbose( - "Extract - create: " + os.path.join(outdir, sub_path)) + "Extract - create: " + os.path.join(outdir, sub_path) + ) self.print_verbose("Extracted source") @@ -959,8 +965,8 @@ class SingletonUpdater: return -1 if self._subfolder_path: - self._subfolder_path.replace('/', os.path.sep) - self._subfolder_path.replace('\\', os.path.sep) + self._subfolder_path.replace("/", os.path.sep) + self._subfolder_path.replace("\\", os.path.sep) # Either directly in root of zip/one subfolder, or use specified path. if not os.path.isfile(os.path.join(unpath, "__init__.py")): @@ -1018,25 +1024,31 @@ class SingletonUpdater: # Make sure that base is not a high level shared folder, but # is dedicated just to the addon itself. self.print_verbose( - "clean=True, clearing addon folder to fresh install state") + "clean=True, clearing addon folder to fresh install state" + ) # Remove root files and folders (except update folder). - files = [f for f in os.listdir(base) - if os.path.isfile(os.path.join(base, f))] - folders = [f for f in os.listdir(base) - if os.path.isdir(os.path.join(base, f))] + files = [ + f for f in os.listdir(base) if os.path.isfile(os.path.join(base, f)) + ] + folders = [ + f for f in os.listdir(base) if os.path.isdir(os.path.join(base, f)) + ] for f in files: os.remove(os.path.join(base, f)) self.print_verbose( - "Clean removing file {}".format(os.path.join(base, f))) + "Clean removing file {}".format(os.path.join(base, f)) + ) for f in folders: if os.path.join(base, f) is self._updater_path: continue shutil.rmtree(os.path.join(base, f)) self.print_verbose( "Clean removing folder and contents {}".format( - os.path.join(base, f))) + os.path.join(base, f) + ) + ) except Exception as err: error = "failed to create clean existing addon folder" @@ -1047,8 +1059,9 @@ class SingletonUpdater: # but avoid removing/altering backup and updater file. for path, dirs, files in os.walk(base): # Prune ie skip updater folder. - dirs[:] = [d for d in dirs - if os.path.join(path, d) not in [self._updater_path]] + dirs[:] = [ + d for d in dirs if os.path.join(path, d) not in [self._updater_path] + ] for file in files: for pattern in self.remove_pre_update_patterns: if fnmatch.filter([file], pattern): @@ -1066,8 +1079,9 @@ class SingletonUpdater: # actual file copying/replacements. for path, dirs, files in os.walk(merger): # Verify structure works to prune updater sub folder overwriting. - dirs[:] = [d for d in dirs - if os.path.join(path, d) not in [self._updater_path]] + dirs[:] = [ + d for d in dirs if os.path.join(path, d) not in [self._updater_path] + ] rel_path = os.path.relpath(path, merger) dest_path = os.path.join(base, rel_path) if not os.path.exists(dest_path): @@ -1090,23 +1104,27 @@ class SingletonUpdater: os.remove(dest_file) os.rename(srcFile, dest_file) self.print_verbose( - "Overwrote file " + os.path.basename(dest_file)) + "Overwrote file " + os.path.basename(dest_file) + ) else: self.print_verbose( "Pattern not matched to {}, not overwritten".format( - os.path.basename(dest_file))) + os.path.basename(dest_file) + ) + ) else: # File did not previously exist, simply move it over. os.rename(srcFile, dest_file) - self.print_verbose( - "New file " + os.path.basename(dest_file)) + self.print_verbose("New file " + os.path.basename(dest_file)) # now remove the temp staging folder and downloaded zip try: shutil.rmtree(staging_path) except: - error = ("Error: Failed to remove existing staging directory, " - "consider manually removing ") + staging_path + error = ( + "Error: Failed to remove existing staging directory, " + "consider manually removing " + ) + staging_path self.print_verbose(error) self.print_trace() @@ -1168,12 +1186,12 @@ class SingletonUpdater: return () segments = list() - tmp = '' + tmp = "" for char in str(text): if not char.isdigit(): if len(tmp) > 0: segments.append(int(tmp)) - tmp = '' + tmp = "" else: tmp += char if len(tmp) > 0: @@ -1184,7 +1202,7 @@ class SingletonUpdater: if not self._include_branches: return () else: - return (text) + return text return tuple(segments) def check_for_update_async(self, callback=None): @@ -1193,7 +1211,8 @@ class SingletonUpdater: self._json is not None and "update_ready" in self._json and self._json["version_text"] != dict() - and self._json["update_ready"]) + and self._json["update_ready"] + ) if is_ready: self._update_ready = True @@ -1210,15 +1229,13 @@ class SingletonUpdater: self.print_verbose("Skipping async check, already started") # already running the bg thread elif self._update_ready is None: - print("{} updater: Running background check for update".format( - self.addon)) + print("{} updater: Running background check for update".format(self.addon)) self.start_async_check_update(False, callback) def check_for_update_now(self, callback=None): self._error = None self._error_msg = None - self.print_verbose( - "Check update pressed, first getting current status") + self.print_verbose("Check update pressed, first getting current status") if self._async_checking: self.print_verbose("Skipping async check, already started") return # already running the bg thread @@ -1243,9 +1260,7 @@ class SingletonUpdater: # avoid running again in, just return past result if found # but if force now check, then still do it if self._update_ready is not None and not now: - return (self._update_ready, - self._update_version, - self._update_link) + return (self._update_ready, self._update_version, self._update_link) if self._current_version is None: raise ValueError("current_version not yet defined") @@ -1259,22 +1274,18 @@ class SingletonUpdater: self.set_updater_json() # self._json if not now and not self.past_interval_timestamp(): - self.print_verbose( - "Aborting check for updated, check interval not reached") + self.print_verbose("Aborting check for updated, check interval not reached") return (False, None, None) # check if using tags or releases # note that if called the first time, this will pull tags from online if self._fake_install: - self.print_verbose( - "fake_install = True, setting fake version as ready") + self.print_verbose("fake_install = True, setting fake version as ready") self._update_ready = True self._update_version = "(999,999,999)" self._update_link = "http://127.0.0.1" - return (self._update_ready, - self._update_version, - self._update_link) + return (self._update_ready, self._update_version, self._update_link) # Primary internet call, sets self._tags and self._tag_latest. self.get_tags() @@ -1327,7 +1338,6 @@ class SingletonUpdater: else: # Situation where branches not included. if new_version > self._current_version: - self._update_ready = True self._update_version = new_version self._update_link = link @@ -1386,8 +1396,7 @@ class SingletonUpdater: if self._fake_install: # Change to True, to trigger the reload/"update installed" handler. self.print_verbose("fake_install=True") - self.print_verbose( - "Just reloading and running any handler triggers") + self.print_verbose("Just reloading and running any handler triggers") self._json["just_updated"] = True self.save_updater_json() if self._backup_current is True: @@ -1401,15 +1410,16 @@ class SingletonUpdater: self.print_verbose("Update stopped, new version not ready") if callback: callback( - self._addon_package, - "Update stopped, new version not ready") + self._addon_package, "Update stopped, new version not ready" + ) return "Update stopped, new version not ready" elif self._update_link is None: # this shouldn't happen if update is ready self.print_verbose("Update stopped, update link unavailable") if callback: - callback(self._addon_package, - "Update stopped, update link unavailable") + callback( + self._addon_package, "Update stopped, update link unavailable" + ) return "Update stopped, update link unavailable" if revert_tag is None: @@ -1461,12 +1471,12 @@ class SingletonUpdater: return True now = datetime.now() - last_check = datetime.strptime( - self._json["last_check"], "%Y-%m-%d %H:%M:%S.%f") + last_check = datetime.strptime(self._json["last_check"], "%Y-%m-%d %H:%M:%S.%f") offset = timedelta( days=self._check_interval_days + 30 * self._check_interval_months, hours=self._check_interval_hours, - minutes=self._check_interval_minutes) + minutes=self._check_interval_minutes, + ) delta = (now - offset) - last_check if delta.total_seconds() > 0: @@ -1482,8 +1492,8 @@ class SingletonUpdater: Will also rename old file paths to addon-specific path if found. """ json_path = os.path.join( - self._updater_path, - "{}_updater_status.json".format(self._addon_package)) + self._updater_path, "{}_updater_status.json".format(self._addon_package) + ) old_json_path = os.path.join(self._updater_path, "updater_status.json") # Rename old file if it exists. @@ -1517,7 +1527,7 @@ class SingletonUpdater: "ignore": False, "just_restored": False, "just_updated": False, - "version_text": dict() + "version_text": dict(), } self.save_updater_json() @@ -1537,11 +1547,13 @@ class SingletonUpdater: jpath = self.get_json_path() if not os.path.isdir(os.path.dirname(jpath)): - print("State error: Directory does not exist, cannot save json: ", - os.path.basename(jpath)) + print( + "State error: Directory does not exist, cannot save json: ", + os.path.basename(jpath), + ) return try: - with open(jpath, 'w') as outf: + with open(jpath, "w") as outf: data_out = json.dumps(self._json, indent=4) outf.write(data_out) except: @@ -1575,8 +1587,13 @@ class SingletonUpdater: if self._async_checking: return self.print_verbose("Starting background checking thread") - check_thread = threading.Thread(target=self.async_check_update, - args=(now, callback,)) + check_thread = threading.Thread( + target=self.async_check_update, + args=( + now, + callback, + ), + ) check_thread.daemon = True self._check_thread = check_thread check_thread.start() @@ -1630,17 +1647,19 @@ class SingletonUpdater: # Updater Engines # ----------------------------------------------------------------------------- + class BitbucketEngine: """Integration to Bitbucket API for git-formatted repositories""" def __init__(self): - self.api_url = 'https://api.bitbucket.org' + self.api_url = "https://api.bitbucket.org" self.token = None self.name = "bitbucket" def form_repo_url(self, updater): return "{}/2.0/repositories/{}/{}".format( - self.api_url, updater.user, updater.repo) + self.api_url, updater.user, updater.repo + ) def form_tags_url(self, updater): return self.form_repo_url(updater) + "/refs/tags?sort=-name" @@ -1650,31 +1669,28 @@ class BitbucketEngine: def get_zip_url(self, name, updater): return "https://bitbucket.org/{user}/{repo}/get/{name}.zip".format( - user=updater.user, - repo=updater.repo, - name=name) + user=updater.user, repo=updater.repo, name=name + ) def parse_tags(self, response, updater): if response is None: return list() return [ - { - "name": tag["name"], - "zipball_url": self.get_zip_url(tag["name"], updater) - } for tag in response["values"]] + {"name": tag["name"], "zipball_url": self.get_zip_url(tag["name"], updater)} + for tag in response["values"] + ] class GithubEngine: """Integration to Github API""" def __init__(self): - self.api_url = 'https://api.github.com' + self.api_url = "https://api.github.com" self.token = None self.name = "github" def form_repo_url(self, updater): - return "{}/repos/{}/{}".format( - self.api_url, updater.user, updater.repo) + return "{}/repos/{}/{}".format(self.api_url, updater.user, updater.repo) def form_tags_url(self, updater): if updater.use_releases: @@ -1698,7 +1714,7 @@ class GitlabEngine: """Integration to GitLab API""" def __init__(self): - self.api_url = 'https://gitlab.com' + self.api_url = "https://gitlab.com" self.token = None self.name = "gitlab" @@ -1710,19 +1726,19 @@ class GitlabEngine: def form_branch_list_url(self, updater): # does not validate branch name. - return "{}/repository/branches".format( - self.form_repo_url(updater)) + return "{}/repository/branches".format(self.form_repo_url(updater)) def form_branch_url(self, branch, updater): # Could clash with tag names and if it does, it will download TAG zip # instead of branch zip to get direct path, would need. return "{}/repository/archive.zip?sha={}".format( - self.form_repo_url(updater), branch) + self.form_repo_url(updater), branch + ) def get_zip_url(self, sha, updater): return "{base}/repository/archive.zip?sha={sha}".format( - base=self.form_repo_url(updater), - sha=sha) + base=self.form_repo_url(updater), sha=sha + ) # def get_commit_zip(self, id, updater): # return self.form_repo_url(updater)+"/repository/archive.zip?sha:"+id @@ -1733,8 +1749,11 @@ class GitlabEngine: return [ { "name": tag["name"], - "zipball_url": self.get_zip_url(tag["commit"]["id"], updater) - } for tag in response] + "zipball_url": self.get_zip_url(tag["commit"]["id"], updater), + } + for tag in response + ] + class ForgejoEngine: """Integration to Forgejo/Gitea API""" @@ -1742,7 +1761,7 @@ class ForgejoEngine: def __init__(self): # the api_url may be overwritten by form_repo_url # if updater.host is set - self.api_url = 'https://codeberg.org' + self.api_url = "https://codeberg.org" self.token = None self.name = "forgejo" @@ -1756,19 +1775,17 @@ class ForgejoEngine: def form_branch_list_url(self, updater): # does not validate branch name. - return "{}/branches".format( - self.form_repo_url(updater)) + return "{}/branches".format(self.form_repo_url(updater)) def form_branch_url(self, branch, updater): # Could clash with tag names and if it does, it will download TAG zip # instead of branch zip to get direct path, would need. - return "{}/archive/{}.zip".format( - self.form_repo_url(updater), branch) + return "{}/archive/{}.zip".format(self.form_repo_url(updater), branch) def get_zip_url(self, sha, updater): return "{base}/archive/{sha}.zip".format( - base=self.form_repo_url(updater), - sha=sha) + base=self.form_repo_url(updater), sha=sha + ) # def get_commit_zip(self, id, updater): # return self.form_repo_url(updater)+"/repository/archive.zip?sha:"+id @@ -1779,8 +1796,11 @@ class ForgejoEngine: return [ { "name": tag["name"], - "zipball_url": self.get_zip_url(tag["commit"]["sha"], updater) - } for tag in response] + "zipball_url": self.get_zip_url(tag["commit"]["sha"], updater), + } + for tag in response + ] + # ----------------------------------------------------------------------------- # The module-shared class instance, diff --git a/addon_updater_ops.py b/addon_updater_ops.py index 0c3e108..0316cc0 100644 --- a/addon_updater_ops.py +++ b/addon_updater_ops.py @@ -83,15 +83,17 @@ def make_annotations(cls): if not hasattr(bpy.app, "version") or bpy.app.version < (2, 80): return cls if bpy.app.version < (2, 93, 0): - bl_props = {k: v for k, v in cls.__dict__.items() - if isinstance(v, tuple)} + bl_props = {k: v for k, v in cls.__dict__.items() if isinstance(v, tuple)} else: - bl_props = {k: v for k, v in cls.__dict__.items() - if isinstance(v, bpy.props._PropertyDeferred)} + bl_props = { + k: v + for k, v in cls.__dict__.items() + if isinstance(v, bpy.props._PropertyDeferred) + } if bl_props: - if '__annotations__' not in cls.__dict__: - setattr(cls, '__annotations__', {}) - annotations = cls.__dict__['__annotations__'] + if "__annotations__" not in cls.__dict__: + setattr(cls, "__annotations__", {}) + annotations = cls.__dict__["__annotations__"] for k, v in bl_props.items(): annotations[k] = v delattr(cls, k) @@ -129,20 +131,23 @@ def get_user_preferences(context=None): # Simple popup to prompt use to check for update & offer install if available. class AddonUpdaterInstallPopup(bpy.types.Operator): """Check and install update if available""" + bl_label = "Update {x} addon".format(x=updater.addon) bl_idname = updater.addon + ".updater_install_popup" bl_description = "Popup to check and display current updates available" - bl_options = {'REGISTER', 'INTERNAL'} + bl_options = {"REGISTER", "INTERNAL"} # if true, run clean install - ie remove all files before adding new # equivalent to deleting the addon and reinstalling, except the # updater folder/backup folder remains clean_install = bpy.props.BoolProperty( name="Clean install", - description=("If enabled, completely clear the addon's folder before " - "installing new update, creating a fresh install"), + description=( + "If enabled, completely clear the addon's folder before " + "installing new update, creating a fresh install" + ), default=False, - options={'HIDDEN'} + options={"HIDDEN"}, ) ignore_enum = bpy.props.EnumProperty( @@ -151,9 +156,9 @@ class AddonUpdaterInstallPopup(bpy.types.Operator): items=[ ("install", "Update Now", "Install update now"), ("ignore", "Ignore", "Ignore this update to prevent future popups"), - ("defer", "Defer", "Defer choice till next blender session") + ("defer", "Defer", "Defer choice till next blender session"), ], - options={'HIDDEN'} + options={"HIDDEN"}, ) def check(self, context): @@ -170,10 +175,11 @@ class AddonUpdaterInstallPopup(bpy.types.Operator): elif updater.update_ready: col = layout.column() col.scale_y = 0.7 - col.label(text="Update {} ready!".format(updater.update_version), - icon="LOOP_FORWARDS") - col.label(text="Choose 'Update Now' & press OK to install, ", - icon="BLANK1") + col.label( + text="Update {} ready!".format(updater.update_version), + icon="LOOP_FORWARDS", + ) + col.label(text="Choose 'Update Now' & press OK to install, ", icon="BLANK1") col.label(text="or click outside window to defer", icon="BLANK1") row = col.row() row.prop(self, "ignore_enum", expand=True) @@ -194,22 +200,21 @@ class AddonUpdaterInstallPopup(bpy.types.Operator): def execute(self, context): # In case of error importing updater. if updater.invalid_updater: - return {'CANCELLED'} + return {"CANCELLED"} if updater.manual_only: bpy.ops.wm.url_open(url=updater.website) elif updater.update_ready: - # Action based on enum selection. - if self.ignore_enum == 'defer': - return {'FINISHED'} - elif self.ignore_enum == 'ignore': + if self.ignore_enum == "defer": + return {"FINISHED"} + elif self.ignore_enum == "ignore": updater.ignore_update() - return {'FINISHED'} + return {"FINISHED"} - res = updater.run_update(force=False, - callback=post_update_callback, - clean=self.clean_install) + res = updater.run_update( + force=False, callback=post_update_callback, clean=self.clean_install + ) # Should return 0, if not something happened. if updater.verbose: @@ -222,84 +227,86 @@ class AddonUpdaterInstallPopup(bpy.types.Operator): # Re-launch this dialog. atr = AddonUpdaterInstallPopup.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT') + getattr(getattr(bpy.ops, atr[0]), atr[1])("INVOKE_DEFAULT") else: updater.print_verbose("Doing nothing, not ready for update") - return {'FINISHED'} + return {"FINISHED"} # User preference check-now operator class AddonUpdaterCheckNow(bpy.types.Operator): bl_label = "Check now for " + updater.addon + " update" bl_idname = updater.addon + ".updater_check_now" - bl_description = "Check now for an update to the {} addon".format( - updater.addon) - bl_options = {'REGISTER', 'INTERNAL'} + bl_description = "Check now for an update to the {} addon".format(updater.addon) + bl_options = {"REGISTER", "INTERNAL"} def execute(self, context): if updater.invalid_updater: - return {'CANCELLED'} + return {"CANCELLED"} if updater.async_checking and updater.error is None: # Check already happened. # Used here to just avoid constant applying settings below. # Ignoring if error, to prevent being stuck on the error screen. - return {'CANCELLED'} + return {"CANCELLED"} # apply the UI settings settings = get_user_preferences(context) if not settings: updater.print_verbose( - "Could not get {} preferences, update check skipped".format( - __package__)) - return {'CANCELLED'} + "Could not get {} preferences, update check skipped".format(__package__) + ) + return {"CANCELLED"} updater.set_check_interval( enabled=settings.auto_check_update, months=settings.updater_interval_months, days=settings.updater_interval_days, hours=settings.updater_interval_hours, - minutes=settings.updater_interval_minutes) + minutes=settings.updater_interval_minutes, + ) # Input is an optional callback function. This function should take a # bool input. If true: update ready, if false: no update ready. updater.check_for_update_now(ui_refresh) - return {'FINISHED'} + return {"FINISHED"} class AddonUpdaterUpdateNow(bpy.types.Operator): bl_label = "Update " + updater.addon + " addon now" bl_idname = updater.addon + ".updater_update_now" bl_description = "Update to the latest version of the {x} addon".format( - x=updater.addon) - bl_options = {'REGISTER', 'INTERNAL'} + x=updater.addon + ) + bl_options = {"REGISTER", "INTERNAL"} # If true, run clean install - ie remove all files before adding new # equivalent to deleting the addon and reinstalling, except the updater # folder/backup folder remains. clean_install = bpy.props.BoolProperty( name="Clean install", - description=("If enabled, completely clear the addon's folder before " - "installing new update, creating a fresh install"), + description=( + "If enabled, completely clear the addon's folder before " + "installing new update, creating a fresh install" + ), default=False, - options={'HIDDEN'} + options={"HIDDEN"}, ) def execute(self, context): - # in case of error importing updater if updater.invalid_updater: - return {'CANCELLED'} + return {"CANCELLED"} if updater.manual_only: bpy.ops.wm.url_open(url=updater.website) if updater.update_ready: # if it fails, offer to open the website instead try: - res = updater.run_update(force=False, - callback=post_update_callback, - clean=self.clean_install) + res = updater.run_update( + force=False, callback=post_update_callback, clean=self.clean_install + ) # Should return 0, if not something happened. if updater.verbose: @@ -312,30 +319,30 @@ class AddonUpdaterUpdateNow(bpy.types.Operator): updater._error_msg = str(expt) updater.print_trace() atr = AddonUpdaterInstallManually.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT') + getattr(getattr(bpy.ops, atr[0]), atr[1])("INVOKE_DEFAULT") elif updater.update_ready is None: (update_ready, version, link) = updater.check_for_update(now=True) # Re-launch this dialog. atr = AddonUpdaterInstallPopup.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT') + getattr(getattr(bpy.ops, atr[0]), atr[1])("INVOKE_DEFAULT") elif not updater.update_ready: - self.report({'INFO'}, "Nothing to update") - return {'CANCELLED'} + self.report({"INFO"}, "Nothing to update") + return {"CANCELLED"} else: - self.report( - {'ERROR'}, "Encountered a problem while trying to update") - return {'CANCELLED'} + self.report({"ERROR"}, "Encountered a problem while trying to update") + return {"CANCELLED"} - return {'FINISHED'} + return {"FINISHED"} class AddonUpdaterUpdateTarget(bpy.types.Operator): bl_label = updater.addon + " version target" bl_idname = updater.addon + ".updater_update_target" bl_description = "Install a targeted version of the {x} addon".format( - x=updater.addon) - bl_options = {'REGISTER', 'INTERNAL'} + x=updater.addon + ) + bl_options = {"REGISTER", "INTERNAL"} def target_version(self, context): # In case of error importing updater. @@ -352,7 +359,7 @@ class AddonUpdaterUpdateTarget(bpy.types.Operator): target = bpy.props.EnumProperty( name="Target version to install", description="Select the version to install", - items=target_version + items=target_version, ) # If true, run clean install - ie remove all files before adding new @@ -360,10 +367,12 @@ class AddonUpdaterUpdateTarget(bpy.types.Operator): # updater folder/backup folder remains. clean_install = bpy.props.BoolProperty( name="Clean install", - description=("If enabled, completely clear the addon's folder before " - "installing new update, creating a fresh install"), + description=( + "If enabled, completely clear the addon's folder before " + "installing new update, creating a fresh install" + ), default=False, - options={'HIDDEN'} + options={"HIDDEN"}, ) @classmethod @@ -389,36 +398,35 @@ class AddonUpdaterUpdateTarget(bpy.types.Operator): def execute(self, context): # In case of error importing updater. if updater.invalid_updater: - return {'CANCELLED'} + return {"CANCELLED"} res = updater.run_update( force=False, revert_tag=self.target, callback=post_update_callback, - clean=self.clean_install) + clean=self.clean_install, + ) # Should return 0, if not something happened. if res == 0: updater.print_verbose("Updater returned successful") else: - updater.print_verbose( - "Updater returned {}, , error occurred".format(res)) - return {'CANCELLED'} + updater.print_verbose("Updater returned {}, , error occurred".format(res)) + return {"CANCELLED"} - return {'FINISHED'} + return {"FINISHED"} class AddonUpdaterInstallManually(bpy.types.Operator): """As a fallback, direct the user to download the addon manually""" + bl_label = "Install update manually" bl_idname = updater.addon + ".updater_install_manually" bl_description = "Proceed to manually install update" - bl_options = {'REGISTER', 'INTERNAL'} + bl_options = {"REGISTER", "INTERNAL"} error = bpy.props.StringProperty( - name="Error Occurred", - default="", - options={'HIDDEN'} + name="Error Occurred", default="", options={"HIDDEN"} ) def invoke(self, context, event): @@ -435,10 +443,8 @@ class AddonUpdaterInstallManually(bpy.types.Operator): if self.error != "": col = layout.column() col.scale_y = 0.7 - col.label(text="There was an issue trying to auto-install", - icon="ERROR") - col.label(text="Press the download button below and install", - icon="BLANK1") + col.label(text="There was an issue trying to auto-install", icon="ERROR") + col.label(text="Press the download button below and install", icon="BLANK1") col.label(text="the zip file like a normal addon.", icon="BLANK1") else: col = layout.column() @@ -454,12 +460,10 @@ class AddonUpdaterInstallManually(bpy.types.Operator): if updater.update_link is not None: row.operator( - "wm.url_open", - text="Direct download").url = updater.update_link + "wm.url_open", text="Direct download" + ).url = updater.update_link else: - row.operator( - "wm.url_open", - text="(failed to retrieve direct download)") + row.operator("wm.url_open", text="(failed to retrieve direct download)") row.enabled = False if updater.website is not None: @@ -471,20 +475,19 @@ class AddonUpdaterInstallManually(bpy.types.Operator): row.label(text="See source website to download the update") def execute(self, context): - return {'FINISHED'} + return {"FINISHED"} class AddonUpdaterUpdatedSuccessful(bpy.types.Operator): """Addon in place, popup telling user it completed or what went wrong""" + bl_label = "Installation Report" bl_idname = updater.addon + ".updater_update_successful" bl_description = "Update installation response" - bl_options = {'REGISTER', 'INTERNAL', 'UNDO'} + bl_options = {"REGISTER", "INTERNAL", "UNDO"} error = bpy.props.StringProperty( - name="Error Occurred", - default="", - options={'HIDDEN'} + name="Error Occurred", default="", options={"HIDDEN"} ) def invoke(self, context, event): @@ -510,9 +513,8 @@ class AddonUpdaterUpdatedSuccessful(bpy.types.Operator): rw = col.row() rw.scale_y = 2 rw.operator( - "wm.url_open", - text="Click for manual download.", - icon="BLANK1").url = updater.website + "wm.url_open", text="Click for manual download.", icon="BLANK1" + ).url = updater.website elif not updater.auto_reload_post_update: # Tell user to restart blender after an update/restore! if "just_restored" in saved and saved["just_restored"]: @@ -521,20 +523,17 @@ class AddonUpdaterUpdatedSuccessful(bpy.types.Operator): alert_row = col.row() alert_row.alert = True alert_row.operator( - "wm.quit_blender", - text="Restart blender to reload", - icon="BLANK1") + "wm.quit_blender", text="Restart blender to reload", icon="BLANK1" + ) updater.json_reset_restore() else: col = layout.column() - col.label( - text="Addon successfully installed", icon="FILE_TICK") + col.label(text="Addon successfully installed", icon="FILE_TICK") alert_row = col.row() alert_row.alert = True alert_row.operator( - "wm.quit_blender", - text="Restart blender to reload", - icon="BLANK1") + "wm.quit_blender", text="Restart blender to reload", icon="BLANK1" + ) else: # reload addon, but still recommend they restart blender @@ -543,28 +542,28 @@ class AddonUpdaterUpdatedSuccessful(bpy.types.Operator): col.scale_y = 0.7 col.label(text="Addon restored", icon="RECOVER_LAST") col.label( - text="Consider restarting blender to fully reload.", - icon="BLANK1") + text="Consider restarting blender to fully reload.", icon="BLANK1" + ) updater.json_reset_restore() else: col = layout.column() col.scale_y = 0.7 + col.label(text="Addon successfully installed", icon="FILE_TICK") col.label( - text="Addon successfully installed", icon="FILE_TICK") - col.label( - text="Consider restarting blender to fully reload.", - icon="BLANK1") + text="Consider restarting blender to fully reload.", icon="BLANK1" + ) def execute(self, context): - return {'FINISHED'} + return {"FINISHED"} class AddonUpdaterRestoreBackup(bpy.types.Operator): """Restore addon from backup""" + bl_label = "Restore backup" bl_idname = updater.addon + ".updater_restore_backup" bl_description = "Restore addon from backup" - bl_options = {'REGISTER', 'INTERNAL'} + bl_options = {"REGISTER", "INTERNAL"} @classmethod def poll(cls, context): @@ -576,17 +575,18 @@ class AddonUpdaterRestoreBackup(bpy.types.Operator): def execute(self, context): # in case of error importing updater if updater.invalid_updater: - return {'CANCELLED'} + return {"CANCELLED"} updater.restore_backup() - return {'FINISHED'} + return {"FINISHED"} class AddonUpdaterIgnore(bpy.types.Operator): """Ignore update to prevent future popups""" + bl_label = "Ignore update" bl_idname = updater.addon + ".updater_ignore" bl_description = "Ignore update to prevent future popups" - bl_options = {'REGISTER', 'INTERNAL'} + bl_options = {"REGISTER", "INTERNAL"} @classmethod def poll(cls, context): @@ -600,25 +600,26 @@ class AddonUpdaterIgnore(bpy.types.Operator): def execute(self, context): # in case of error importing updater if updater.invalid_updater: - return {'CANCELLED'} + return {"CANCELLED"} updater.ignore_update() self.report({"INFO"}, "Open addon preferences for updater options") - return {'FINISHED'} + return {"FINISHED"} class AddonUpdaterEndBackground(bpy.types.Operator): """Stop checking for update in the background""" + bl_label = "End background check" bl_idname = updater.addon + ".end_background_check" bl_description = "Stop checking for update in the background" - bl_options = {'REGISTER', 'INTERNAL'} + bl_options = {"REGISTER", "INTERNAL"} def execute(self, context): # in case of error importing updater if updater.invalid_updater: - return {'CANCELLED'} + return {"CANCELLED"} updater.stop_async_check_update() - return {'FINISHED'} + return {"FINISHED"} # ----------------------------------------------------------------------------- @@ -645,16 +646,16 @@ def updater_run_success_popup_handler(scene): try: if "scene_update_post" in dir(bpy.app.handlers): - bpy.app.handlers.scene_update_post.remove( - updater_run_success_popup_handler) + bpy.app.handlers.scene_update_post.remove(updater_run_success_popup_handler) else: bpy.app.handlers.depsgraph_update_post.remove( - updater_run_success_popup_handler) + updater_run_success_popup_handler + ) except: pass atr = AddonUpdaterUpdatedSuccessful.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT') + getattr(getattr(bpy.ops, atr[0]), atr[1])("INVOKE_DEFAULT") @persistent @@ -669,11 +670,11 @@ def updater_run_install_popup_handler(scene): try: if "scene_update_post" in dir(bpy.app.handlers): - bpy.app.handlers.scene_update_post.remove( - updater_run_install_popup_handler) + bpy.app.handlers.scene_update_post.remove(updater_run_install_popup_handler) else: bpy.app.handlers.depsgraph_update_post.remove( - updater_run_install_popup_handler) + updater_run_install_popup_handler + ) except: pass @@ -687,12 +688,12 @@ def updater_run_install_popup_handler(scene): # User probably manually installed to get the up to date addon # in here. Clear out the update flag using this function. updater.print_verbose( - "{} updater: appears user updated, clearing flag".format( - updater.addon)) + "{} updater: appears user updated, clearing flag".format(updater.addon) + ) updater.json_reset_restore() return atr = AddonUpdaterInstallPopup.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT') + getattr(getattr(bpy.ops, atr[0]), atr[1])("INVOKE_DEFAULT") def background_update_callback(update_ready): @@ -720,11 +721,9 @@ def background_update_callback(update_ready): return if "scene_update_post" in dir(bpy.app.handlers): # 2.7x - bpy.app.handlers.scene_update_post.append( - updater_run_install_popup_handler) + bpy.app.handlers.scene_update_post.append(updater_run_install_popup_handler) else: # 2.8+ - bpy.app.handlers.depsgraph_update_post.append( - updater_run_install_popup_handler) + bpy.app.handlers.depsgraph_update_post.append(updater_run_install_popup_handler) ran_auto_check_install_popup = True updater.print_verbose("Attempted popup prompt") @@ -748,17 +747,18 @@ def post_update_callback(module_name, res=None): # This is the same code as in conditional at the end of the register # function, ie if "auto_reload_post_update" == True, skip code. updater.print_verbose( - "{} updater: Running post update callback".format(updater.addon)) + "{} updater: Running post update callback".format(updater.addon) + ) atr = AddonUpdaterUpdatedSuccessful.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT') + getattr(getattr(bpy.ops, atr[0]), atr[1])("INVOKE_DEFAULT") global ran_update_success_popup ran_update_success_popup = True else: # Some kind of error occurred and it was unable to install, offer # manual download instead. atr = AddonUpdaterUpdatedSuccessful.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT', error=res) + getattr(getattr(bpy.ops, atr[0]), atr[1])("INVOKE_DEFAULT", error=res) return @@ -791,11 +791,13 @@ def check_for_update_background(): settings = get_user_preferences(bpy.context) if not settings: return - updater.set_check_interval(enabled=settings.auto_check_update, - months=settings.updater_interval_months, - days=settings.updater_interval_days, - hours=settings.updater_interval_hours, - minutes=settings.updater_interval_minutes) + updater.set_check_interval( + enabled=settings.auto_check_update, + months=settings.updater_interval_months, + days=settings.updater_interval_days, + hours=settings.updater_interval_hours, + minutes=settings.updater_interval_minutes, + ) # Input is an optional callback function. This function should take a bool # input, if true: update ready, if false: no update ready. @@ -813,22 +815,25 @@ def check_for_update_nonthreaded(self, context): settings = get_user_preferences(bpy.context) if not settings: if updater.verbose: - print("Could not get {} preferences, update check skipped".format( - __package__)) + print( + "Could not get {} preferences, update check skipped".format(__package__) + ) return - updater.set_check_interval(enabled=settings.auto_check_update, - months=settings.updater_interval_months, - days=settings.updater_interval_days, - hours=settings.updater_interval_hours, - minutes=settings.updater_interval_minutes) + updater.set_check_interval( + enabled=settings.auto_check_update, + months=settings.updater_interval_months, + days=settings.updater_interval_days, + hours=settings.updater_interval_hours, + minutes=settings.updater_interval_minutes, + ) (update_ready, version, link) = updater.check_for_update(now=False) if update_ready: atr = AddonUpdaterInstallPopup.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT') + getattr(getattr(bpy.ops, atr[0]), atr[1])("INVOKE_DEFAULT") else: updater.print_verbose("No update ready") - self.report({'INFO'}, "No update ready") + self.report({"INFO"}, "No update ready") def show_reload_popup(): @@ -866,11 +871,9 @@ def show_reload_popup(): return if "scene_update_post" in dir(bpy.app.handlers): # 2.7x - bpy.app.handlers.scene_update_post.append( - updater_run_success_popup_handler) + bpy.app.handlers.scene_update_post.append(updater_run_success_popup_handler) else: # 2.8+ - bpy.app.handlers.depsgraph_update_post.append( - updater_run_success_popup_handler) + bpy.app.handlers.depsgraph_update_post.append(updater_run_success_popup_handler) ran_update_success_popup = True @@ -896,10 +899,7 @@ def update_notice_box_ui(self, context): col = box.column() alert_row = col.row() alert_row.alert = True - alert_row.operator( - "wm.quit_blender", - text="Restart blender", - icon="ERROR") + alert_row.operator("wm.quit_blender", text="Restart blender", icon="ERROR") col.label(text="to complete update") return @@ -924,13 +924,13 @@ def update_notice_box_ui(self, context): colR = split.column(align=True) colR.scale_y = 1.5 if not updater.manual_only: - colR.operator(AddonUpdaterUpdateNow.bl_idname, - text="Update", icon="LOOP_FORWARDS") + colR.operator( + AddonUpdaterUpdateNow.bl_idname, text="Update", icon="LOOP_FORWARDS" + ) col.operator("wm.url_open", text="Open website").url = updater.website # ops = col.operator("wm.url_open",text="Direct download") # ops.url=updater.update_link - col.operator(AddonUpdaterInstallManually.bl_idname, - text="Install manually") + col.operator(AddonUpdaterInstallManually.bl_idname, text="Install manually") else: # ops = col.operator("wm.url_open", text="Direct download") # ops.url=updater.update_link @@ -959,7 +959,7 @@ def update_settings_ui(self, context, element=None): return settings = get_user_preferences(context) if not settings: - box.label(text="Error getting updater preferences", icon='ERROR') + box.label(text="Error getting updater preferences", icon="ERROR") return # auto-update settings @@ -971,9 +971,11 @@ def update_settings_ui(self, context, element=None): saved_state = updater.json if "just_updated" in saved_state and saved_state["just_updated"]: row.alert = True - row.operator("wm.quit_blender", - text="Restart blender to complete update", - icon="ERROR") + row.operator( + "wm.quit_blender", + text="Restart blender to complete update", + icon="ERROR", + ) return split = layout_split(row, factor=0.4) @@ -1007,16 +1009,13 @@ def update_settings_ui(self, context, element=None): split.scale_y = 2 if "ssl" in updater.error_msg.lower(): split.enabled = True - split.operator(AddonUpdaterInstallManually.bl_idname, - text=updater.error) + split.operator(AddonUpdaterInstallManually.bl_idname, text=updater.error) else: split.enabled = False - split.operator(AddonUpdaterCheckNow.bl_idname, - text=updater.error) + split.operator(AddonUpdaterCheckNow.bl_idname, text=updater.error) split = sub_col.split(align=True) split.scale_y = 2 - split.operator(AddonUpdaterCheckNow.bl_idname, - text="", icon="FILE_REFRESH") + split.operator(AddonUpdaterCheckNow.bl_idname, text="", icon="FILE_REFRESH") elif updater.update_ready is None and not updater.async_checking: col.scale_y = 2 @@ -1032,61 +1031,62 @@ def update_settings_ui(self, context, element=None): split.scale_y = 2 split.operator(AddonUpdaterEndBackground.bl_idname, text="", icon="X") - elif updater.include_branches and \ - len(updater.tags) == len(updater.include_branch_list) and not \ - updater.manual_only: + elif ( + updater.include_branches + and len(updater.tags) == len(updater.include_branch_list) + and not updater.manual_only + ): # No releases found, but still show the appropriate branch. sub_col = col.row(align=True) sub_col.scale_y = 1 split = sub_col.split(align=True) split.scale_y = 2 - update_now_txt = "Update directly to {}".format( - updater.include_branch_list[0]) + update_now_txt = "Update directly to {}".format(updater.include_branch_list[0]) split.operator(AddonUpdaterUpdateNow.bl_idname, text=update_now_txt) split = sub_col.split(align=True) split.scale_y = 2 - split.operator(AddonUpdaterCheckNow.bl_idname, - text="", icon="FILE_REFRESH") + split.operator(AddonUpdaterCheckNow.bl_idname, text="", icon="FILE_REFRESH") elif updater.update_ready and not updater.manual_only: sub_col = col.row(align=True) sub_col.scale_y = 1 split = sub_col.split(align=True) split.scale_y = 2 - split.operator(AddonUpdaterUpdateNow.bl_idname, - text="Update now to " + str(updater.update_version)) + split.operator( + AddonUpdaterUpdateNow.bl_idname, + text="Update now to " + str(updater.update_version), + ) split = sub_col.split(align=True) split.scale_y = 2 - split.operator(AddonUpdaterCheckNow.bl_idname, - text="", icon="FILE_REFRESH") + split.operator(AddonUpdaterCheckNow.bl_idname, text="", icon="FILE_REFRESH") elif updater.update_ready and updater.manual_only: col.scale_y = 2 dl_now_txt = "Download " + str(updater.update_version) - col.operator("wm.url_open", - text=dl_now_txt).url = updater.website + col.operator("wm.url_open", text=dl_now_txt).url = updater.website else: # i.e. that updater.update_ready == False. sub_col = col.row(align=True) sub_col.scale_y = 1 split = sub_col.split(align=True) split.enabled = False split.scale_y = 2 - split.operator(AddonUpdaterCheckNow.bl_idname, - text="Addon is up to date") + split.operator(AddonUpdaterCheckNow.bl_idname, text="Addon is up to date") split = sub_col.split(align=True) split.scale_y = 2 - split.operator(AddonUpdaterCheckNow.bl_idname, - text="", icon="FILE_REFRESH") + split.operator(AddonUpdaterCheckNow.bl_idname, text="", icon="FILE_REFRESH") if not updater.manual_only: col = row.column(align=True) if updater.include_branches and len(updater.include_branch_list) > 0: branch = updater.include_branch_list[0] - col.operator(AddonUpdaterUpdateTarget.bl_idname, - text="Install {} / old version".format(branch)) + col.operator( + AddonUpdaterUpdateTarget.bl_idname, + text="Install {} / old version".format(branch), + ) else: - col.operator(AddonUpdaterUpdateTarget.bl_idname, - text="(Re)install addon version") + col.operator( + AddonUpdaterUpdateTarget.bl_idname, text="(Re)install addon version" + ) last_date = "none found" backup_path = os.path.join(updater.stage_path, "backup") if "backup_date" in updater.json and os.path.isdir(backup_path): @@ -1103,7 +1103,7 @@ def update_settings_ui(self, context, element=None): if updater.error is not None and updater.error_msg is not None: row.label(text=updater.error_msg) elif last_check: - last_check = last_check[0: last_check.index(".")] + last_check = last_check[0 : last_check.index(".")] row.label(text="Last update check: " + last_check) else: row.label(text="Last update check: Never") @@ -1127,7 +1127,7 @@ def update_settings_ui_condensed(self, context, element=None): return settings = get_user_preferences(context) if not settings: - row.label(text="Error getting updater preferences", icon='ERROR') + row.label(text="Error getting updater preferences", icon="ERROR") return # Special case to tell user to restart blender, if set that way. @@ -1138,7 +1138,8 @@ def update_settings_ui_condensed(self, context, element=None): row.operator( "wm.quit_blender", text="Restart blender to complete update", - icon="ERROR") + icon="ERROR", + ) return col = row.column() @@ -1149,16 +1150,13 @@ def update_settings_ui_condensed(self, context, element=None): split.scale_y = 2 if "ssl" in updater.error_msg.lower(): split.enabled = True - split.operator(AddonUpdaterInstallManually.bl_idname, - text=updater.error) + split.operator(AddonUpdaterInstallManually.bl_idname, text=updater.error) else: split.enabled = False - split.operator(AddonUpdaterCheckNow.bl_idname, - text=updater.error) + split.operator(AddonUpdaterCheckNow.bl_idname, text=updater.error) split = sub_col.split(align=True) split.scale_y = 2 - split.operator(AddonUpdaterCheckNow.bl_idname, - text="", icon="FILE_REFRESH") + split.operator(AddonUpdaterCheckNow.bl_idname, text="", icon="FILE_REFRESH") elif updater.update_ready is None and not updater.async_checking: col.scale_y = 2 @@ -1174,9 +1172,11 @@ def update_settings_ui_condensed(self, context, element=None): split.scale_y = 2 split.operator(AddonUpdaterEndBackground.bl_idname, text="", icon="X") - elif updater.include_branches and \ - len(updater.tags) == len(updater.include_branch_list) and not \ - updater.manual_only: + elif ( + updater.include_branches + and len(updater.tags) == len(updater.include_branch_list) + and not updater.manual_only + ): # No releases found, but still show the appropriate branch. sub_col = col.row(align=True) sub_col.scale_y = 1 @@ -1186,20 +1186,20 @@ def update_settings_ui_condensed(self, context, element=None): split.operator(AddonUpdaterUpdateNow.bl_idname, text=now_txt) split = sub_col.split(align=True) split.scale_y = 2 - split.operator(AddonUpdaterCheckNow.bl_idname, - text="", icon="FILE_REFRESH") + split.operator(AddonUpdaterCheckNow.bl_idname, text="", icon="FILE_REFRESH") elif updater.update_ready and not updater.manual_only: sub_col = col.row(align=True) sub_col.scale_y = 1 split = sub_col.split(align=True) split.scale_y = 2 - split.operator(AddonUpdaterUpdateNow.bl_idname, - text="Update now to " + str(updater.update_version)) + split.operator( + AddonUpdaterUpdateNow.bl_idname, + text="Update now to " + str(updater.update_version), + ) split = sub_col.split(align=True) split.scale_y = 2 - split.operator(AddonUpdaterCheckNow.bl_idname, - text="", icon="FILE_REFRESH") + split.operator(AddonUpdaterCheckNow.bl_idname, text="", icon="FILE_REFRESH") elif updater.update_ready and updater.manual_only: col.scale_y = 2 @@ -1211,12 +1211,10 @@ def update_settings_ui_condensed(self, context, element=None): split = sub_col.split(align=True) split.enabled = False split.scale_y = 2 - split.operator(AddonUpdaterCheckNow.bl_idname, - text="Addon is up to date") + split.operator(AddonUpdaterCheckNow.bl_idname, text="Addon is up to date") split = sub_col.split(align=True) split.scale_y = 2 - split.operator(AddonUpdaterCheckNow.bl_idname, - text="", icon="FILE_REFRESH") + split.operator(AddonUpdaterCheckNow.bl_idname, text="", icon="FILE_REFRESH") row = element.row() row.prop(settings, "auto_check_update") @@ -1227,7 +1225,7 @@ def update_settings_ui_condensed(self, context, element=None): if updater.error is not None and updater.error_msg is not None: row.label(text=updater.error_msg) elif last_check != "" and last_check is not None: - last_check = last_check[0: last_check.index(".")] + last_check = last_check[0 : last_check.index(".")] row.label(text="Last check: " + last_check) else: row.label(text="Last check: Never") @@ -1328,7 +1326,7 @@ classes = ( AddonUpdaterUpdatedSuccessful, AddonUpdaterRestoreBackup, AddonUpdaterIgnore, - AddonUpdaterEndBackground + AddonUpdaterEndBackground, ) @@ -1396,7 +1394,13 @@ def register(bl_info): updater.backup_current = True # True by default # Sample ignore patterns for when creating backup of current during update. - updater.backup_ignore_patterns = [".git", "__pycache__", "*.bat", ".gitignore", "*.exe"] + updater.backup_ignore_patterns = [ + ".git", + "__pycache__", + "*.bat", + ".gitignore", + "*.exe", + ] # Alternate example patterns: # updater.backup_ignore_patterns = [".git", "__pycache__", "*.bat", ".gitignore", "*.exe"] @@ -1465,7 +1469,7 @@ def register(bl_info): # Note: updater.include_branch_list defaults to ['master'] branch if set to # none. Example targeting another multiple branches allowed to pull from: # updater.include_branch_list = ['master', 'dev'] - updater.include_branch_list = ['main', 'dev'] # None is the equivalent = ['master'] + updater.include_branch_list = ["main", "dev"] # None is the equivalent = ['master'] # Only allow manual install, thus prompting the user to open # the addon's web page to download, specifically: updater.website diff --git a/bimport.py b/bimport.py index 2ec6f58..27cd8b5 100644 --- a/bimport.py +++ b/bimport.py @@ -1,13 +1,15 @@ import bpy -from bpy.props import (StringProperty, - BoolProperty, - EnumProperty, - IntProperty, - FloatProperty, - CollectionProperty) +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 io_scene_gltf2 import ConvertGLTF2_Base import importlib # then import dependencies for our addon @@ -22,43 +24,52 @@ else: from .common import utils -# taken from blender_git/blender/scripts/addons/io_scene_gltf2/__init__.py +# 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': [] } + 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"] == "metrics") \ - and not (f"{utils.prefix()}_type" in node.extras and node.extras[f"{utils.prefix()}_type"] == "metrics"): + if ( + type(node.extras) != type(None) + and "glyph" in node.extras + and not ("type" in node.extras and node.extras["type"] == "metrics") + and not ( + f"{utils.prefix()}_type" in node.extras + and node.extras[f"{utils.prefix()}_type"] == "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 + +# 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'} + 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") + # 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): @@ -70,96 +81,106 @@ class GetFontFacesInFile(Operator, ImportHelper): if self.files: # Multiple file check - ret = {'CANCELLED'} + 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'} + 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'} + 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'}) + 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") + loglevel: IntProperty(name="Log Level", description="Log Level") import_pack_images: BoolProperty( - name='Pack Images', - description='Pack all images into .blend file', - default=True + name="Pack Images", description="Pack all images into .blend file", default=True ) merge_vertices: BoolProperty( - name='Merge Vertices', + 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' + "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", "")), + items=( + ("NORMALS", "Use Normal Data", ""), + ("FLAT", "Flat Shading", ""), + ("SMOOTH", "Smooth Shading", ""), + ), description="How normals are computed during import", - default="NORMALS") + default="NORMALS", + ) bone_heuristic: EnumProperty( name="Bone Dir", items=( - ("BLENDER", "Blender (best for import/export round trip)", + ( + "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)", + "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)", + "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"), + "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', + 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' + "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', + name="Import WebP textures", description=( "If a texture exists in WebP format, " "loads the WebP texture instead of the fallback PNG/JPEG one" @@ -168,7 +189,7 @@ class ImportGLTF2(Operator, ConvertGLTF2_Base, ImportHelper): ) glyphs: StringProperty( - name='Import only these glyphs', + 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" @@ -197,25 +218,32 @@ class ImportGLTF2(Operator, ConvertGLTF2_Base, ImportHelper): 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') + 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()) + 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 + self.has_active_importer_extensions = ( + len(importer_extension_panel_unregister_functors) > 0 + ) return ImportHelper.invoke(self, context, event) def execute(self, context): @@ -230,25 +258,26 @@ class ImportGLTF2(Operator, ConvertGLTF2_Base, ImportHelper): 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'): + if hasattr(module, "glTF2ImportUserExtension"): extension_ctor = module.glTF2ImportUserExtension user_extensions.append(extension_ctor()) - import_settings['import_user_extensions'] = user_extensions + import_settings["import_user_extensions"] = user_extensions if self.files: # Multiple file import - ret = {'CANCELLED'} + 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'} + if self.unit_import(path, import_settings) == {"FINISHED"}: + ret = {"FINISHED"} return ret else: # Single file import @@ -308,18 +337,31 @@ class ImportGLTF2(Operator, ConvertGLTF2_Base, ImportHelper): # 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 ( + 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 @@ -355,7 +397,7 @@ class ImportGLTF2(Operator, ConvertGLTF2_Base, ImportHelper): # and some have different indices for node in nodes: if type(node.children) != type(None): - children = [] # brand new children + children = [] # brand new children for i, c in enumerate(node.children): # check if children are lost if c in node_indices: @@ -399,23 +441,26 @@ class ImportGLTF2(Operator, ConvertGLTF2_Base, ImportHelper): 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"): + 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") and \ - not (f"{utils.prefix()}_type" in n_vars["extras"] and \ - n_vars["extras"][f"{utils.prefix()}_type"] == "metrics"): + if ( + "glyph" in n_vars["extras"] + and not ( + "type" in n_vars["extras"] + and n_vars["extras"]["type"] == "metrics" + ) + and not ( + f"{utils.prefix()}_type" in n_vars["extras"] + and n_vars["extras"][f"{utils.prefix()}_type"] + == "metrics" + ) + ): obj["type"] = "glyph" for vi, vnode in gltf.vnodes.items(): @@ -432,14 +477,15 @@ class ImportGLTF2(Operator, ConvertGLTF2_Base, ImportHelper): if hasattr(gltf.log.logger, "removeHandler"): gltf.log.logger.removeHandler(gltf.log_handler) - return {'FINISHED'} + return {"FINISHED"} except ImportError as e: - self.report({'ERROR'}, e.args[0]) - return {'CANCELLED'} + 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: diff --git a/common/Font.py b/common/Font.py index 1c8368e..f01c91b 100644 --- a/common/Font.py +++ b/common/Font.py @@ -7,64 +7,64 @@ from pathlib import Path # note: overwritten/extended by the content of "glypNamesToUnicode.txt" # when addon is registered in __init__.py name_to_glyph_d = { - "zero": "0", - "one": "1", - "two": "2", - "three": "3", - "four": "4", - "five": "5", - "six": "6", - "seven": "7", - "eight": "8", - "nine": "9", - "ampersand": "&", - "backslash": "\\", - "colon": ":", - "comma": ",", - "equal": "=", - "exclam": "!", - "hyphen": "-", - "minus": "−", - "parenleft": "(", - "parenright": "(", - "period": ".", - "plus": "+", - "question": "?", - "quotedblleft": "“", - "quotedblright": "”", - "semicolon": ";", - "slash": "/", - "space": " ", - } + "zero": "0", + "one": "1", + "two": "2", + "three": "3", + "four": "4", + "five": "5", + "six": "6", + "seven": "7", + "eight": "8", + "nine": "9", + "ampersand": "&", + "backslash": "\\", + "colon": ":", + "comma": ",", + "equal": "=", + "exclam": "!", + "hyphen": "-", + "minus": "−", + "parenleft": "(", + "parenright": "(", + "period": ".", + "plus": "+", + "question": "?", + "quotedblleft": "“", + "quotedblright": "”", + "semicolon": ";", + "slash": "/", + "space": " ", +} space_d = {} known_misspellings = { - # simple misspelling - "excent" : "accent", - "overdot" : "dotaccent", - "diaresis": "dieresis", - "diaeresis": "dieresis", - # character does not exist.. maybe something else - "Odoubleacute": "Ohungarumlaut", - "Udoubleacute": "Uhungarumlaut", - "Wcaron": "Wcircumflex", - "Neng": "Nlongrightleg", - "Lgrave": "Lacute", - # currency stuff - "doller": "dollar", - "euro": "Euro", - "yuan": "yen", # https://en.wikipedia.org/wiki/Yen_and_yuan_sign - "pound": "sterling", - # whoopsie - "__": "_", - } + # simple misspelling + "excent": "accent", + "overdot": "dotaccent", + "diaresis": "dieresis", + "diaeresis": "dieresis", + # character does not exist.. maybe something else + "Odoubleacute": "Ohungarumlaut", + "Udoubleacute": "Uhungarumlaut", + "Wcaron": "Wcircumflex", + "Neng": "Nlongrightleg", + "Lgrave": "Lacute", + # currency stuff + "doller": "dollar", + "euro": "Euro", + "yuan": "yen", # https://en.wikipedia.org/wiki/Yen_and_yuan_sign + "pound": "sterling", + # whoopsie + "__": "_", +} + def fix_glyph_name_misspellings(name): for misspelling in known_misspellings: if misspelling in name: - return name.replace(misspelling, - known_misspellings[misspelling]) + return name.replace(misspelling, known_misspellings[misspelling]) return name @@ -88,33 +88,37 @@ def generate_from_file_d(filepath): d = {} with open(filepath) as f: for line in f: - if line[0] == '#': + if line[0] == "#": continue - split = line.split(' ') + split = line.split(" ") if len(split) == 2: - (name, hexstr) = line.split(' ') + (name, hexstr) = line.split(" ") val = chr(int(hexstr, base=16)) d[name] = val if len(split) == 3: # we might have a parameter, like for the spaces - (name, hexstr, parameter) = line.split(' ') + (name, hexstr, parameter) = line.split(" ") parameter_value = float(parameter) val = chr(int(hexstr, base=16)) d[name] = [val, parameter_value] return d + def generate_name_to_glyph_d(): return generate_from_file_d(f"{Path(__file__).parent}/glyphNamesToUnicode.txt") + def generate_space_d(): return generate_from_file_d(f"{Path(__file__).parent}/spacesUnicode.txt") + def init(): global name_to_glyph_d global space_d name_to_glyph_d = generate_name_to_glyph_d() space_d = generate_space_d() + class FontFace: """FontFace is a class holding glyphs @@ -127,8 +131,8 @@ class FontFace: :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. @@ -139,13 +143,15 @@ class FontFace: self.filepaths = [] self.unit_factor = 1.0 + class Font: """Font holds the faces and various metadata for a font :param faces: dictionary of faces, defaults to ``Dict[str, FontFace]`` :type faces: Dict[str, FontFace] """ - def __init__(self, faces = Dict[str, FontFace]): + + def __init__(self, faces=Dict[str, FontFace]): self.faces = faces @@ -156,14 +162,18 @@ def register_font(font_name, face_name, glyphs_in_fontfile, filepath): 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)) + 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 + """add_glyph adds a glyph to a FontFace it creates the :class:`Font` and :class:`FontFace` if it does not exist yet :param font_name: The Font you want to add the glyph to @@ -187,8 +197,9 @@ def add_glyph(font_name, face_name, glyph_id, 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 + """add_glyph adds a glyph to a FontFace it creates the :class:`Font` and :class:`FontFace` if it does not exist yet :param font_name: The :class:`Font` you want to get the glyph from @@ -219,14 +230,17 @@ def get_glyph(font_name, face_name, glyph_id, alternate=0): 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 # , , + if ( + not fonts.keys().__contains__(font_name) + or fonts[font_name].faces.get(face_name) == None + ): + return "", "", text # , , loaded = [] missing = [] @@ -240,36 +254,45 @@ def test_glyphs_availability(font_name, face_name, text): 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 + return ( + "".join(loaded), + "".join(missing), + "".join(maybe), + fonts[font_name].faces[face_name].filepaths, + ) + def get_loaded_fonts(): return fonts.keys() + def get_loaded_fonts_and_faces(): out = [] for f in fonts.keys(): for ff in fonts[f].faces.keys(): - out.append([f,ff]) + out.append([f, ff]) return out + MISSING_FONT = 0 MISSING_FACE = 1 + def test_availability(font_name, face_name, text): if not fonts.keys().__contains__(font_name): return MISSING_FONT if fonts[font_name].faces.get(face_name) == None: return MISSING_FACE - loaded, missing, maybe, filepaths = test_glyphs_availability(font_name, - face_name, - text) + loaded, missing, maybe, filepaths = test_glyphs_availability( + font_name, face_name, text + ) return { - "loaded": loaded, - "missing": missing, - "maybe": maybe, - "filepaths": filepaths, - } - + "loaded": loaded, + "missing": missing, + "maybe": maybe, + "filepaths": filepaths, + } + # holds all fonts fonts = {} From abdee651e0b91af2a056217c94b2b3c98a7e9c6b Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Tue, 13 May 2025 16:24:54 +0200 Subject: [PATCH 032/103] enable ale why not --- _vimrc_local.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_vimrc_local.vim b/_vimrc_local.vim index 1733d50..cac1787 100644 --- a/_vimrc_local.vim +++ b/_vimrc_local.vim @@ -17,4 +17,4 @@ let g:jedi#environment_path = "venv" "let g:ale_linters = { 'javascript': ['eslint', 'tsserver'], 'python': ['jedils', 'pylint', 'flake8'], 'cpp': ['cc', 'clangcheck', 'clangd', 'clangtidy', 'clazy', 'cppcheck', 'cpplint', 'cquery', 'cspell', 'flawfinder'], 'php': ['php_cs_fixer'] } "let g:ale_fixers = { '*': ['remove_trailing_lines', 'trim_whitespace'], 'python': ['autopep8'], 'cpp': ['uncrustify'], 'javascript': js_fixers, 'css': ['prettier'], 'json': ['prettier'], 'php': ['php_cs_fixer'] } -let g:ale_pattern_options = {'\.py$': {'ale_enabled': 0}} +"let g:ale_pattern_options = {'\.py$': {'ale_enabled': 0}} From a3c7172573c99622a5a0739b7114e42383256d65 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Tue, 13 May 2025 16:25:19 +0200 Subject: [PATCH 033/103] cleanup --- __init__.py | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/__init__.py b/__init__.py index f8c2e0a..8c4597f 100644 --- a/__init__.py +++ b/__init__.py @@ -4,14 +4,14 @@ A 3D font helper """ -import functools import importlib -import io import os import bpy from bpy.app.handlers import persistent -from bpy.types import Panel + +from . import addon_updater_ops, bimport, butils +from .common import Font, utils bl_info = { "name": "ABC3D", @@ -24,9 +24,6 @@ bl_info = { } # NOTE: also change version in common/utils.py -from . import addon_updater_ops, bimport, butils -from .common import Font, utils - # make sure that modules are reloadable # when registering # handy for development @@ -127,10 +124,6 @@ class ABC3D_addonPreferences(bpy.types.AddonPreferences): layout.label(text="Directory for storage of fonts and other assets:") layout.prop(self, "assets_dir") - # Works best if a column, or even just self.layout. - mainrow = layout.row() - col = mainrow.column() - # Updater draw function, could also pass in col as third arg. addon_updater_ops.update_settings_ui(self, context) @@ -148,7 +141,6 @@ class ABC3D_glyph_properties(bpy.types.PropertyGroup): description="Letter Spacing", ) - class ABC3D_text_properties(bpy.types.PropertyGroup): def font_items_callback(self, context): items = [] @@ -158,7 +150,6 @@ class ABC3D_text_properties(bpy.types.PropertyGroup): def font_default_callback(self, context): d = context.scene.abc3d_data - items = self.font_items_callback(context) if len(d.available_fonts) > 0: if len(d.available_fonts) > d.active_text_index: f = d.available_fonts[d.active_text_index] @@ -167,7 +158,7 @@ class ABC3D_text_properties(bpy.types.PropertyGroup): f = d.available_fonts[0] return 0 # f"{f.font_name} {f.face_name}" - if type(self.font_name) != type(None) and type(self.face_name) != type(None): + 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 # "" @@ -481,7 +472,7 @@ class ABC3D_PT_TextManagement(bpy.types.Panel): for c in t.text_object.children: if ( len(c.users_collection) > 0 - and (c.get(f"{utils.prefix()}_linked_textobject")) != type(None) + and not isinstance(c.get(f"{utils.prefix()}_linked_textobject"), None) and c.get(f"{utils.prefix()}_linked_textobject") == t.text_id ): remove_me = False From f41808adc3ae4918a8b2fb4b654dad5cb8a1ba70 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Tue, 13 May 2025 16:27:56 +0200 Subject: [PATCH 034/103] [fix] remove infix --- __init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__init__.py b/__init__.py index 8c4597f..43e02a6 100644 --- a/__init__.py +++ b/__init__.py @@ -1439,7 +1439,7 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): print(f"processing {o.name}") process_object = True if self.autodetect_names: - font_name, face_name, input_infix = self.do_autodetect_names(o.name) + font_name, face_name = self.do_autodetect_names(o.name) if butils.is_mesh(o) and not butils.is_metrics_object(o): uc = o.users_collection From 9276ad4fac1702a05fa7944894d53a9cfd20c787 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Tue, 13 May 2025 16:59:17 +0200 Subject: [PATCH 035/103] [fix] scene variable to global this prevents failure of access in case the scene is refreshed by blender --- __init__.py | 9 +++------ butils.py | 9 ++++++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/__init__.py b/__init__.py index 43e02a6..30affd8 100644 --- a/__init__.py +++ b/__init__.py @@ -1652,15 +1652,12 @@ def on_depsgraph_update(scene, depsgraph): lock_depsgraph_updates() def later(): - if ( - "lock_depsgraph_update_ntimes" not in scene.abc3d_data - or scene.abc3d_data["lock_depsgraph_update_ntimes"] <= 0 - ): + if butils.lock_depsgraph_update_n_times <= 0: butils.set_text_on_curve( scene.abc3d_data.available_texts[linked_textobject] ) - elif scene.abc3d_data["lock_depsgraph_update_ntimes"] > 0: - scene.abc3d_data["lock_depsgraph_update_ntimes"] -= 1 + elif butils.lock_depsgraph_update_n_times <= 0: + butils.lock_depsgraph_update_n_times -= 1 butils.run_in_main_thread(later) diff --git a/butils.py b/butils.py index d3b6940..aeb8cfe 100644 --- a/butils.py +++ b/butils.py @@ -21,6 +21,7 @@ else: from .common import utils execution_queue = queue.Queue() +lock_depsgraph_update_n_times = -1 # This function can safely be called in another thread. @@ -811,6 +812,8 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) :type reset_depsgraph_n: int """ + global lock_depsgraph_update_n_times + # starttime = time.perf_counter_ns() mom = text_properties.text_object if mom.type != "CURVE": @@ -1040,12 +1043,12 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) mom[f"{utils.prefix()}_orientation"] = text_properties.orientation mom[f"{utils.prefix()}_translation"] = text_properties.translation - if "lock_depsgraph_update_ntimes" in bpy.context.scene.abc3d_data: - bpy.context.scene.abc3d_data["lock_depsgraph_update_ntimes"] += len( + if lock_depsgraph_update_n_times < 0: + lock_depsgraph_update_n_times = len( bpy.context.selected_objects ) else: - bpy.context.scene.abc3d_data["lock_depsgraph_update_ntimes"] = len( + lock_depsgraph_update_n_times += len( bpy.context.selected_objects ) From c063c7af1d8902e79dc1267a96e19d53a409f4bf Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Tue, 13 May 2025 16:59:32 +0200 Subject: [PATCH 036/103] cleanup formatting etc --- butils.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/butils.py b/butils.py index aeb8cfe..06a77bf 100644 --- a/butils.py +++ b/butils.py @@ -2,7 +2,6 @@ import importlib import os import queue import re -from multiprocessing import Process import bpy import mathutils @@ -132,7 +131,6 @@ def calc_tangent_on_bezier(bezier_point_1, bezier_point_2, t): ).normalized() -from math import acos, pi, radians, sqrt def align_rotations_auto_pivot( @@ -403,7 +401,7 @@ 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(f"found more glyphs objects than expected") + print("found more glyphs objects than expected") # now it must exist glyphs_obj = find_objects_by_name(face_obj.children, startswith="glyphs")[0] @@ -445,9 +443,9 @@ def register_font_from_filepath(filepath): font_name = a["font_name"] face_name = a["face_name"] glyph = a["glyph"] - if not font_name in fonts: + if font_name not in fonts: fonts[font_name] = {} - if not face_name in fonts[font_name]: + if face_name not in fonts[font_name]: fonts[font_name][face_name] = [] fonts[font_name][face_name].append(glyph) for font_name in fonts: @@ -460,7 +458,7 @@ def register_font_from_filepath(filepath): def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""): if not filepath.endswith(".glb") and not filepath.endswith(".gltf"): ShowMessageBox( - f"Font loading error", + "Font loading error", "ERROR", f"Filepath({filepath}) is not a *.glb or *.gltf file", ) @@ -524,7 +522,7 @@ def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""): update_available_fonts() remove_list = [] for o in bpy.context.scene.collection.all_objects: - if not o.name in fontcollection.all_objects: + if o.name not in fontcollection.all_objects: if marker_property in o and o[marker_property] == True: remove_list.append(o) @@ -656,13 +654,13 @@ def completely_delete_objects(objs, recursive=True): try: if hasattr(g, "children") and len(g.children) > 0: completely_delete_objects(g.children) - except ReferenceError as e: + except ReferenceError: # not important pass try: bpy.data.objects.remove(g, do_unlink=True) - except ReferenceError as e: + except ReferenceError: # not important pass @@ -699,7 +697,7 @@ def is_glyph(o): and is_mesh(o) and not is_metrics_object(o) ) - except ReferenceError as e: + except ReferenceError: return False @@ -1442,7 +1440,7 @@ def align_metrics_of_objects_to_active_object(objects=None): # do it for o in objects: is_possibly_glyph = is_glyph(o) - if is_possibly_glyph and not o is bpy.context.active_object: + if is_possibly_glyph and o is not bpy.context.active_object: metrics = [] for c in o.children: if is_metrics_object(c): From 47bc10df3f82c5087ba97fdcbba2f6d701ed6daa Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Tue, 13 May 2025 17:28:45 +0200 Subject: [PATCH 037/103] fix first wrong placement --- butils.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/butils.py b/butils.py index 06a77bf..e792156 100644 --- a/butils.py +++ b/butils.py @@ -936,9 +936,10 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) is_newline = True if regenerate: - ob.location = mom.matrix_world @ ( - location + text_properties.translation - ) + # ob.location = mom.matrix_world @ ( + # location + text_properties.translation + # ) + ob.location = location + text_properties.translation mom.users_collection[0].objects.link(obg) mom.users_collection[0].objects.link(ob) ob.parent = mom From 16bdfc8cc6b510919577bdff1b6811aa09f2da4d Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Tue, 13 May 2025 17:29:12 +0200 Subject: [PATCH 038/103] cosmetics --- butils.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/butils.py b/butils.py index e792156..924ae35 100644 --- a/butils.py +++ b/butils.py @@ -866,9 +866,9 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) glyph_tmp = Font.get_glyph( text_properties.font_name, text_properties.face_name, glyph_id ) - if glyph_tmp == None: + if glyph_tmp is None: space_width = Font.is_space(glyph_id) - if space_width != False: + if space_width: advance = advance + space_width * text_properties.font_size continue @@ -881,7 +881,7 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) text_properties.face_name, possible_replacement, ) - if glyph_tmp != None: + if glyph_tmp is not None: message = message + f" (replaced with '{possible_replacement}')" replaced = True @@ -891,7 +891,7 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) message=message, prevent_repeat=True, ) - if replaced == False: + if not replaced: continue glyph = glyph_tmp.original @@ -987,7 +987,7 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) # now we need to compensate for curvature # otherwise letters will be closer together the curvier the bezier is - # this could be done more efficiently, but whatever + # NOTE: this could be done more efficiently curve_compensation = 0 if distribution_type == "CALCULATE" and ( not is_newline or spline_index == 0 From 79c0a563f13e0a3b73826e8463ad7d8326e6e5f3 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Tue, 13 May 2025 17:29:28 +0200 Subject: [PATCH 039/103] add fallback --- butils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/butils.py b/butils.py index 924ae35..9f3243e 100644 --- a/butils.py +++ b/butils.py @@ -308,8 +308,9 @@ def calc_point_on_bezier_curve( ) total_length += length - # TODO: can this fail? - + # NOTE: this is a fallback + # 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)] From 8609db1597931766ab67252bdf966588a6eeee85 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Tue, 13 May 2025 18:02:02 +0200 Subject: [PATCH 040/103] hide Nulls --- butils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/butils.py b/butils.py index 9f3243e..b6e51f6 100644 --- a/butils.py +++ b/butils.py @@ -900,6 +900,7 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) obg = None if regenerate: ob = bpy.data.objects.new(f"{glyph_id}", None) + ob.hide_viewport = True obg = bpy.data.objects.new(f"{glyph_id}_mesh", glyph.data) ob[f"{utils.prefix()}_type"] = "glyph" ob[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id From c95e010f819e36e39c221c7354fe9dce9e65c6de Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Tue, 13 May 2025 18:02:36 +0200 Subject: [PATCH 041/103] fix ignore_orientation --- butils.py | 42 +++++++++++++++++------------------------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/butils.py b/butils.py index b6e51f6..1c4e0c3 100644 --- a/butils.py +++ b/butils.py @@ -950,33 +950,25 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) else: ob.location = location + text_properties.translation - if not text_properties.ignore_orientation: - mask = [0] - input_rotations = [mathutils.Vector((0.0, 0.0, 0.0))] - 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 - ) + # orientation / rotation + mask = [0] + input_rotations = [mathutils.Vector((0.0, 0.0, 0.0))] + 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()] - q = mathutils.Quaternion() - q.rotate(text_properties.orientation) - if regenerate: - obg.rotation_quaternion = q - ob.rotation_quaternion = ( - mom.matrix_world @ motor[0] - ).to_quaternion() - else: - ob.rotation_quaternion = motor[0].to_quaternion() - else: - q = mathutils.Quaternion() - q.rotate(text_properties.orientation) - # obg.rotation_quaternion = q - obg.rotation_quaternion = ( - mom.matrix_world @ q.to_matrix().to_4x4() + q = mathutils.Quaternion() + q.rotate(text_properties.orientation) + if regenerate: + obg.rotation_quaternion = q + ob.rotation_quaternion = ( + mom.matrix_world @ motor[0] ).to_quaternion() - # ob.rotation_quaternion = (mom.matrix_world @ q.to_matrix().to_4x4()).to_quaternion() + else: + ob.rotation_quaternion = motor[0].to_quaternion() if previous_ob_rotation_mode: ob.rotation_mode = previous_ob_rotation_mode From 19c86420f825f657db5be064fa2579b84b9f382e Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Wed, 14 May 2025 17:21:26 +0200 Subject: [PATCH 042/103] fix closed splines --- butils.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/butils.py b/butils.py index 1c4e0c3..81a03f1 100644 --- a/butils.py +++ b/butils.py @@ -228,10 +228,15 @@ def calc_point_on_bezier_spline( lengths = [] total_length = 0 n_bezier_points = len(bezier_spline_obj.bezier_points) - for i in range(0, len(bezier_spline_obj.bezier_points) - 1): + real_n_bezier_points = len(bezier_spline_obj.bezier_points) + if bezier_spline_obj.use_cyclic_u: + n_bezier_points += 1 + for i in range(0, n_bezier_points - 1): + i_a = i % (n_bezier_points - 1) + i_b = (i_a + 1) % real_n_bezier_points bezier = [ - bezier_spline_obj.bezier_points[i], - bezier_spline_obj.bezier_points[i + 1], + bezier_spline_obj.bezier_points[i_a], + bezier_spline_obj.bezier_points[i_b], ] length = calc_bezier_length( bezier[0], From 73d7a5689775fd5f7ac3f49ce833ee661f27f3bf Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Wed, 14 May 2025 17:21:36 +0200 Subject: [PATCH 043/103] fix rotation issue --- butils.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/butils.py b/butils.py index 81a03f1..9ddfba1 100644 --- a/butils.py +++ b/butils.py @@ -967,13 +967,18 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) q = mathutils.Quaternion() q.rotate(text_properties.orientation) - if regenerate: - obg.rotation_quaternion = q - ob.rotation_quaternion = ( - mom.matrix_world @ motor[0] - ).to_quaternion() - else: - ob.rotation_quaternion = motor[0].to_quaternion() + ob.rotation_quaternion = (motor[0].to_3x3() @ q.to_matrix()).to_quaternion() + # if regenerate: + # obg.rotation_quaternion = q + # ob.rotation_quaternion = ( + # mom.matrix_world @ motor[0] + # ).to_quaternion() + # else: + # ob.rotation_quaternion = motor[0].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: + # ob.rotation_quaternion = (mom.matrix_world.inverted().to_3x3() @ motor[0].to_3x3() @ q.to_matrix()).to_quaternion() if previous_ob_rotation_mode: ob.rotation_mode = previous_ob_rotation_mode From 05371c367543339fa4cd7416e777add04f4d466d Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Wed, 14 May 2025 17:21:45 +0200 Subject: [PATCH 044/103] fix timer --- __init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/__init__.py b/__init__.py index 30affd8..ad76f8b 100644 --- a/__init__.py +++ b/__init__.py @@ -1301,6 +1301,7 @@ class ABC3D_OT_SaveFontToFile(bpy.types.Operator): if obj["font_name"] == selected_font.font_name: if butils.is_metrics_object(obj): butils.remove_faces_from_metrics(obj) + return None bpy.app.timers.register(lambda: remove_faces(), first_interval=2) self.report({"INFO"}, "did it") From 1d4fece0ea8327422b4d7352fbbaa0d549904ccc Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Wed, 14 May 2025 17:22:04 +0200 Subject: [PATCH 045/103] fix naming when creating font --- __init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/__init__.py b/__init__.py index ad76f8b..9d5b8be 100644 --- a/__init__.py +++ b/__init__.py @@ -1358,7 +1358,8 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): row = layout.row() row.prop(self, "autodetect_names") first_object_name = context.selected_objects[-1].name - self.font_name, self.face_name = self.do_autodetect_names(first_object_name) + if self.autodetect_names: + self.font_name, self.face_name = self.do_autodetect_names(first_object_name) if self.autodetect_names: scale_y = 0.5 row = layout.row() @@ -1450,8 +1451,9 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): # glyph_id = Font.name_to_glyph(name) name = o.name.split("_")[0] glyph_id = Font.name_to_glyph(name) + o.name = f"{name}_{font_name}_{face_name}" - if type(glyph_id) != type(None): + if glyph_id is not None: o["glyph"] = glyph_id o["font_name"] = font_name o["face_name"] = face_name From 71dda9f316c34f99fc1f0b94600915b2b381a455 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Wed, 14 May 2025 17:30:03 +0200 Subject: [PATCH 046/103] bump version to v0.0.5 in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9e500d6..0b0bdee 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ / ___ \| |_) | |___ ___) | |_| | /_/ \_\____/ \____|____/|____/ ``` -v0.0.4 +v0.0.5 Convenience tool to work with 3D typography in Blender and Cinema4D. From da382f5fab80c421c9029f28486e7b2eea526ee3 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sun, 18 May 2025 13:40:05 +0200 Subject: [PATCH 047/103] add glyph_to_name --- common/Font.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/common/Font.py b/common/Font.py index f01c91b..5dfbd0d 100644 --- a/common/Font.py +++ b/common/Font.py @@ -77,6 +77,13 @@ def name_to_glyph(name): return None +def glyph_to_name(glyph_id): + for k in name_to_glyph_d: + if glyph_id == name_to_glyph_d[k]: + return k + return glyph_id + + def is_space(character): for name in space_d: if character == space_d[name][0]: From be5f060c98fc3cb220ff2f7945907f63ea042d5b Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sun, 18 May 2025 13:42:22 +0200 Subject: [PATCH 048/103] add naming helper convenience function for naming glyphs --- __init__.py | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/__init__.py b/__init__.py index 9d5b8be..c18dcf0 100644 --- a/__init__.py +++ b/__init__.py @@ -564,6 +564,95 @@ class ABC3D_PT_TextManagement(bpy.types.Panel): layout.row().operator(f"{__name__}.remove_text", text="Remove Textobject") +class ABC3D_PG_FontCreation(bpy.types.PropertyGroup): + bl_label = "Font Creation Properties" + # bl_parent_id = "ABC3D_PG_NamingHelper" + # bl_category = "ABC3D" + # bl_space_type = "VIEW_3D" + # bl_region_type = "UI" + # bl_options = {"DEFAULT_CLOSED"} + + def naming_glyph_id_update_callback(self, context): + glyph_name = Font.glyph_to_name(self.naming_glyph_id) + if self.naming_glyph_full: + self.naming_glyph_name = f"{glyph_name}_{self.font_name}_{self.face_name}" + else: + self.naming_glyph_name = glyph_name + + naming_glyph_id: bpy.props.StringProperty( + name="", + description="find proper naming for a glyph", + default="", + maxlen=32, + update=naming_glyph_id_update_callback, + ) + + naming_glyph_name: bpy.props.StringProperty( + name="", + description="find proper naming for a glyph", + default="", + maxlen=1024, + ) + + naming_glyph_full: bpy.props.BoolProperty( + default=True, + description="Generate full name", + update=naming_glyph_id_update_callback, + ) + + font_name: bpy.props.StringProperty( + name="", + description="Font name", + default="NM_Origin", + update=naming_glyph_id_update_callback, + ) + + face_name: bpy.props.StringProperty( + name="", + description="FontFace name", + default="Tender", + 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" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + abc3d_font_creation = context.scene.abc3d_font_creation + name = abc3d_font_creation.naming_glyph_name + context.active_object.name = name + return {"FINISHED"} + + +class ABC3D_PT_NamingHelper(bpy.types.Panel): + bl_label = "Naming Helper" + bl_parent_id = "ABC3D_PT_FontCreation" + bl_category = "ABC3D" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_options = {"DEFAULT_CLOSED"} + + def draw(self, context): + layout = self.layout + scene = context.scene + + abc3d_font_creation = scene.abc3d_font_creation + + box = layout.box() + box.label(text="Glyph Naming Helper") + box.row().prop(abc3d_font_creation, "naming_glyph_full") + box.label(text="Glyph Character Input") + box.row().prop(abc3d_font_creation, "naming_glyph_id") + box.label(text="Font name:") + box.row().prop(abc3d_font_creation, "font_name") + box.label(text="FontFace name:") + 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") + class ABC3D_PT_FontCreation(bpy.types.Panel): bl_label = "Font Creation" bl_parent_id = "ABC3D_PT_Panel" @@ -1517,6 +1606,9 @@ classes = ( ABC3D_PT_TextPlacement, ABC3D_PT_TextManagement, ABC3D_PT_FontCreation, + ABC3D_PG_FontCreation, + ABC3D_OT_NamingHelper, + ABC3D_PT_NamingHelper, ABC3D_PT_TextPropertiesPanel, ABC3D_OT_OpenAssetDirectory, ABC3D_OT_LoadInstalledFonts, @@ -1673,6 +1765,7 @@ 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.Object.__del__ = lambda self: print(f"Bye {self.name}") # autostart if we load a blend file @@ -1716,6 +1809,7 @@ def unregister(): bpy.app.handlers.depsgraph_update_post.remove(on_depsgraph_update) del bpy.types.Scene.abc3d_data + del bpy.types.Scene.abc3d_font_creation print(f"UNREGISTER {utils.prefix()}") From 34eeb4af9472d338cefceb374f25497df714de3f Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sun, 18 May 2025 13:43:50 +0200 Subject: [PATCH 049/103] align object origins convenience function to align origins --- __init__.py | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/__init__.py b/__init__.py index c18dcf0..48a03af 100644 --- a/__init__.py +++ b/__init__.py @@ -691,6 +691,10 @@ class ABC3D_PT_FontCreation(bpy.types.Panel): layout.row().operator( 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_metrics", text="Align origins to Metrics (left)") + # box.row().operator(f"{__name__}.fix_objects_metrics_origins", text="Fix objects metrics origins") class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): @@ -1012,6 +1016,72 @@ 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.""" + + bl_idname = f"{__name__}.align_origins_to_active_object" + 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') + + 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. + + # 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.""" + + # 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, + # ) + + # 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 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. + + # 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"} + + # 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.""" @@ -1617,6 +1687,9 @@ classes = ( ABC3D_OT_RemoveMetrics, ABC3D_OT_AlignMetricsToActiveObject, ABC3D_OT_AlignMetrics, + ABC3D_OT_AlignOriginsToActiveObject, + # ABC3D_OT_AlignOriginsToMetrics, + # ABC3D_OT_FixObjectsMetricsOrigins, ABC3D_OT_TemporaryHelper, ABC3D_OT_RemoveText, ABC3D_OT_PlaceText, From 6708fd0491bb16dcd70b2cb52d6d6c40722d741f Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sun, 18 May 2025 17:21:23 +0200 Subject: [PATCH 050/103] conventions --- common/Font.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/Font.py b/common/Font.py index 5dfbd0d..7cf3eec 100644 --- a/common/Font.py +++ b/common/Font.py @@ -45,9 +45,9 @@ known_misspellings = { "overdot": "dotaccent", "diaresis": "dieresis", "diaeresis": "dieresis", + # different conventions + "doubleacute": "hungarumlaut", # character does not exist.. maybe something else - "Odoubleacute": "Ohungarumlaut", - "Udoubleacute": "Uhungarumlaut", "Wcaron": "Wcircumflex", "Neng": "Nlongrightleg", "Lgrave": "Lacute", From ed5db9361333316f4a83c0e9ae703ead3a50c537 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sun, 18 May 2025 17:21:33 +0200 Subject: [PATCH 051/103] cleanup --- common/Font.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/common/Font.py b/common/Font.py index 7cf3eec..bcb7949 100644 --- a/common/Font.py +++ b/common/Font.py @@ -1,6 +1,4 @@ -from typing import TypedDict from typing import Dict -from dataclasses import dataclass from pathlib import Path # convenience dictionary for translating names to glyph ids From d88c0c50cd37923b2a32f8276d340cede516542f Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sun, 18 May 2025 17:22:31 +0200 Subject: [PATCH 052/103] cleanup --- __init__.py | 4 +--- butils.py | 4 +++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/__init__.py b/__init__.py index 48a03af..c49f2f5 100644 --- a/__init__.py +++ b/__init__.py @@ -664,9 +664,6 @@ class ABC3D_PT_FontCreation(bpy.types.Panel): def draw(self, context): layout = self.layout wm = context.window_manager - scene = context.scene - - abc3d_data = scene.abc3d_data layout.row().operator( f"{__name__}.toggle_abc3d_collection", text="Toggle Collection" @@ -677,6 +674,7 @@ class ABC3D_PT_FontCreation(bpy.types.Panel): layout.row().operator( f"{__name__}.save_font_to_file", text="Export Font To File" ) + box = layout.box() box.label(text="metrics") box.row().operator( diff --git a/butils.py b/butils.py index 9ddfba1..3158e2d 100644 --- a/butils.py +++ b/butils.py @@ -485,7 +485,7 @@ def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""): modified_font_faces = [] all_glyph_os = [] - all_objects = [] + for o in bpy.context.scene.objects: if marker_property in o: if "type" in o and o["type"] == "glyph": @@ -847,6 +847,7 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) glyph_advance = 0 is_command = False previous_spline_index = -1 + for i, c in enumerate(text_properties.text): face = Font.fonts[text_properties.font_name].faces[text_properties.face_name] scalor = face.unit_factor * text_properties.font_size @@ -899,6 +900,7 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) ) if not replaced: continue + glyph = glyph_tmp.original ob = None From 6943a9189c5a60f04d8eef36bd09a7b76134ae3a Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sun, 18 May 2025 17:22:48 +0200 Subject: [PATCH 053/103] fix shadowing variable --- butils.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/butils.py b/butils.py index 3158e2d..76cd32c 100644 --- a/butils.py +++ b/butils.py @@ -511,17 +511,17 @@ def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""): modified_font_faces.append({"font_name": font_name, "face_name": face_name}) for mff in modified_font_faces: - glyphs = [] + mff_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(get_original(glyph)) - if len(glyphs) > 0: - add_default_metrics_to_objects(glyphs) + mff_glyphs.append(get_original(glyph)) + if len(mff_glyphs) > 0: + add_default_metrics_to_objects(mff_glyphs) # calculate unit factor - h = get_glyph_height(glyphs[0]) + h = get_glyph_height(mff_glyphs[0]) if h != 0: face.unit_factor = 1 / h From d56ca84236f9d7f0c9476179c2275e8036c397ae Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sun, 18 May 2025 17:23:38 +0200 Subject: [PATCH 054/103] glyph properties, orientation fixes --- __init__.py | 67 +++++++- butils.py | 428 ++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 359 insertions(+), 136 deletions(-) diff --git a/__init__.py b/__init__.py index c49f2f5..8666c7a 100644 --- a/__init__.py +++ b/__init__.py @@ -134,11 +134,26 @@ class ABC3D_available_font(bpy.types.PropertyGroup): 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] + ) + glyph_id: bpy.props.StringProperty(maxlen=1) + text_id: bpy.props.IntProperty( + default=-1, + ) + alternate: bpy.props.IntProperty( + default=-1, + update=update_callback, + ) glyph_object: bpy.props.PointerProperty(type=bpy.types.Object) letter_spacing: bpy.props.FloatProperty( name="Letter Spacing", description="Letter Spacing", + update=update_callback, ) class ABC3D_text_properties(bpy.types.PropertyGroup): @@ -704,14 +719,38 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): def get_active_text_properties(self): # and bpy.context.object.select_get(): - if type(bpy.context.active_object) != type(None): - for t in bpy.context.scene.abc3d_data.available_texts: - if bpy.context.active_object == t.text_object: - return t - if bpy.context.active_object.parent == t.text_object: - return t + a_o = bpy.context.active_object + if a_o is not None: + if f"{utils.prefix()}_linked_textobject" in a_o: + text_index = a_o[f"{utils.prefix()}_linked_textobject"] + return bpy.context.scene.abc3d_data.available_texts[text_index] + elif f"{utils.prefix()}_linked_textobject" in a_o.parent: + text_index = a_o.parent[f"{utils.prefix()}_linked_textobject"] + 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): + return t return None + # NOTE: HERE + def get_active_glyph_properties(self): + a_o = bpy.context.active_object + if a_o is not None: + if (f"{utils.prefix()}_linked_textobject" in a_o + and f"{utils.prefix()}_glyph_index" in a_o): + text_index = a_o[f"{utils.prefix()}_linked_textobject"] + glyph_index = a_o[f"{utils.prefix()}_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): + for g in t.glyphs: + 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() @@ -741,7 +780,7 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): @classmethod def poll(self, context): - return type(self.get_active_text_properties(self)) != type(None) + return self.get_active_text_properties(self) is not None def draw(self, context): layout = self.layout @@ -750,11 +789,16 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): abc3d_data = scene.abc3d_data props = self.get_active_text_properties() + glyph_props = self.get_active_glyph_properties() - if type(props) == type(None) or type(props.text_object) == type(None): + if props is None or props.text_object is None: # this should not happen # as then polling does not work # however, we are paranoid + if props is None: + layout.label(text="props is none") + elif props.text_object is None: + layout.label(text="props.text_object is none") return layout.label(text=f"Mom: {props.text_object.name}") @@ -768,6 +812,13 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): layout.column().prop(props, "translation") layout.column().prop(props, "orientation") + if glyph_props is None: + return + box = layout.box() + box.label(text=f"{glyph_props.glyph_id}") + box.row().prop(glyph_props, "letter_spacing") + + class ABC3D_OT_InstallFont(bpy.types.Operator): """Install or load Fontfile from path above. diff --git a/butils.py b/butils.py index 76cd32c..94ed804 100644 --- a/butils.py +++ b/butils.py @@ -733,6 +733,12 @@ 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): + return -1 * c.bound_box[0][0], c.bound_box[4][0] + return -1 * glyph_obj.bound_box[0][0], glyph_obj.bound_box[4][0] + def get_glyph_height(glyph_obj): for c in glyph_obj.children: @@ -802,6 +808,41 @@ def will_regenerate(text_properties): return False +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 + +def is_or_has_parent(o, parent, if_is_parent=True, max_depth=10): + if o == parent and if_is_parent: + return True + for i in range(0, max_depth): + o = o.parent + if o == parent: + return True + if o is None: + return False + return False + +def parent_to_curve(o, c): + 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': + 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): """set_text_on_curve @@ -815,8 +856,9 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) :param reset_depsgraph_n: reset external parameters after n-th depsgraph update. (<= 0) = immediate, (> 0) = reset after n-th depsgraph update, (False) = no depsgraph reset :type reset_depsgraph_n: int """ - - global lock_depsgraph_update_n_times + # NOTE: depsgraph update not locked + # as we fixed data_path with parent_to_curve trick + # global lock_depsgraph_update_n_times # starttime = time.perf_counter_ns() mom = text_properties.text_object @@ -825,26 +867,41 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) 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 + # # use_path messes with parenting # however, we need it for follow_path # https://projects.blender.org/blender/blender/issues/100661 - previous_use_path = mom.data.use_path - if distribution_type == "CALCULATE": - mom.data.use_path = False - elif distribution_type == "FOLLOW_PATH": - mom.data.use_path = True + # previous_use_path = mom.data.use_path + # if distribution_type == "CALCULATE": + # mom.data.use_path = False + # elif distribution_type == "FOLLOW_PATH": + # mom.data.use_path = True regenerate = will_regenerate(text_properties) # if we regenerate.... delete objects if regenerate and text_properties.get("glyphs"): + for g in text_properties.glyphs: + print(dict(g)) glyph_objects = [g["glyph_object"] for g in text_properties["glyphs"]] completely_delete_objects(glyph_objects, True) text_properties.glyphs.clear() + mom[f"{utils.prefix()}_type"] = "textobject" + mom[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id + mom[f"{utils.prefix()}_font_name"] = text_properties.font_name + mom[f"{utils.prefix()}_face_name"] = text_properties.face_name + mom[f"{utils.prefix()}_font_size"] = text_properties.font_size + mom[f"{utils.prefix()}_letter_spacing"] = text_properties.letter_spacing + mom[f"{utils.prefix()}_orientation"] = text_properties.orientation + mom[f"{utils.prefix()}_translation"] = text_properties.translation + curve_length = get_curve_length(mom) advance = text_properties.offset glyph_advance = 0 + glyph_index = 0 is_command = False previous_spline_index = -1 @@ -860,7 +917,6 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) is_newline = True next_line_advance = get_next_line_advance(mom, advance, glyph_advance) if advance == next_line_advance: - # self.report({'INFO'}, f"would like to add new line for {text_properties.text} please") print( f"would like to add new line for {text_properties.text} please" ) @@ -870,9 +926,14 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) is_command = False glyph_id = c - glyph_tmp = Font.get_glyph( - text_properties.font_name, text_properties.face_name, glyph_id - ) + spline_index = 0 + + ############### GET GLYPH + + 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: @@ -887,6 +948,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 ) if glyph_tmp is not None: message = message + f" (replaced with '{possible_replacement}')" @@ -903,59 +965,82 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) glyph = glyph_tmp.original - ob = None - obg = None + ############### GLYPH PROPERTIES + + glyph_properties = text_properties.glyphs[glyph_index] if not regenerate else text_properties.glyphs.add() + if regenerate: - ob = bpy.data.objects.new(f"{glyph_id}", None) - ob.hide_viewport = True - obg = bpy.data.objects.new(f"{glyph_id}_mesh", glyph.data) - ob[f"{utils.prefix()}_type"] = "glyph" - ob[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id - ob[f"{utils.prefix()}_font_name"] = text_properties.font_name - ob[f"{utils.prefix()}_face_name"] = text_properties.face_name + glyph_properties["glyph_id"] = glyph_id + glyph_properties["text_id"] = text_properties.text_id + glyph_properties["letter_spacing"] = 0 + + ############### NODE SCENE MANAGEMENT + + inner_node = None + outer_node = None + if regenerate: + outer_node = bpy.data.objects.new(f"{glyph_id}", None) + inner_node = bpy.data.objects.new(f"{glyph_id}_mesh", glyph.data) + outer_node[f"{utils.prefix()}_type"] = "glyph" + outer_node[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id + outer_node[f"{utils.prefix()}_glyph_index"] = glyph_index + outer_node[f"{utils.prefix()}_font_name"] = text_properties.font_name + outer_node[f"{utils.prefix()}_face_name"] = text_properties.face_name + + # Add into the scene. + mom.users_collection[0].objects.link(outer_node) + mom.users_collection[0].objects.link(inner_node) + # bpy.context.scene.collection.objects.link(inner_node) + + # Parenting is hard. + 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) + + glyph_properties["glyph_object"] = outer_node else: - ob = text_properties.glyphs[i].glyph_object - for c in ob.children: + outer_node = glyph_properties.glyph_object + outer_node[f"{utils.prefix()}_glyph_index"] = glyph_index + for c in outer_node.children: if c.name.startswith(f"{glyph_id}_mesh"): - obg = c + inner_node = c + + ############### TRANSFORMS + + # origins could be shifted + # so we need to apply a pre_advance + glyph_pre_advance, glyph_post_advance = get_glyph_prepost_advances(glyph) + advance += glyph_pre_advance * scalor if distribution_type == "FOLLOW_PATH": - ob.constraints.new(type="FOLLOW_PATH") - ob.constraints["Follow Path"].target = mom - ob.constraints["Follow Path"].use_fixed_location = True - ob.constraints["Follow Path"].offset_factor = advance / curve_length - ob.constraints["Follow Path"].use_curve_follow = True - ob.constraints["Follow Path"].forward_axis = "FORWARD_X" - ob.constraints["Follow Path"].up_axis = "UP_Y" + 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 = 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" spline_index = 0 elif distribution_type == "CALCULATE": - previous_ob_rotation_mode = None - previous_obg_rotation_mode = None - if ob.rotation_mode != "QUATERNION": - ob.rotation_mode = "QUATERNION" - previous_ob_rotation_mode = ob.rotation_mode - if obg.rotation_mode != "QUATERNION": - obg.rotation_mode = "QUATERNION" - previous_obg_rotation_mode = obg.rotation_mode + previous_outer_node_rotation_mode = None + previous_inner_node_rotation_mode = None + if outer_node.rotation_mode != "QUATERNION": + outer_node.rotation_mode = "QUATERNION" + previous_outer_node_rotation_mode = outer_node.rotation_mode + if inner_node.rotation_mode != "QUATERNION": + inner_node.rotation_mode = "QUATERNION" + previous_inner_node_rotation_mode = inner_node.rotation_mode - location, tangent, spline_index = calc_point_on_bezier_curve( - mom, advance, True, True - ) + # get info from bezier + location, tangent, spline_index = calc_point_on_bezier_curve(mom, advance, True, True) + + # check if we are on a new line if spline_index != previous_spline_index: is_newline = True - if regenerate: - # ob.location = mom.matrix_world @ ( - # location + text_properties.translation - # ) - ob.location = location + text_properties.translation - mom.users_collection[0].objects.link(obg) - mom.users_collection[0].objects.link(ob) - ob.parent = mom - obg.parent = ob - obg.location = mathutils.Vector((0.0, 0.0, 0.0)) - else: - ob.location = location + text_properties.translation + # position + outer_node.location = location + text_properties.translation # orientation / rotation mask = [0] @@ -969,26 +1054,28 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) q = mathutils.Quaternion() q.rotate(text_properties.orientation) - ob.rotation_quaternion = (motor[0].to_3x3() @ q.to_matrix()).to_quaternion() - # if regenerate: - # obg.rotation_quaternion = q - # ob.rotation_quaternion = ( - # mom.matrix_world @ motor[0] - # ).to_quaternion() - # else: - # ob.rotation_quaternion = motor[0].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: - # ob.rotation_quaternion = (mom.matrix_world.inverted().to_3x3() @ 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: + # outer_node.rotation_quaternion = (mom.matrix_world.inverted().to_3x3() @ motor[0].to_3x3() @ q.to_matrix()).to_quaternion() - if previous_ob_rotation_mode: - ob.rotation_mode = previous_ob_rotation_mode - if previous_obg_rotation_mode: - obg.rotation_mode = previous_obg_rotation_mode + # # scale + outer_node.scale = (scalor, scalor, scalor) + if previous_outer_node_rotation_mode: + outer_node.rotation_mode = previous_outer_node_rotation_mode + if previous_inner_node_rotation_mode: + inner_node.rotation_mode = previous_inner_node_rotation_mode + + # outer_node.hide_viewport = True + outer_node.hide_set(True) + + ############### PREPARE FOR THE NEXT + + print(f"{glyph_id}: {glyph_properties.letter_spacing=}") glyph_advance = ( - get_glyph_advance(glyph) * scalor + text_properties.letter_spacing + glyph_post_advance * scalor + text_properties.letter_spacing + glyph_properties.letter_spacing ) # now we need to compensate for curvature @@ -997,7 +1084,7 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) curve_compensation = 0 if distribution_type == "CALCULATE" and ( not is_newline or spline_index == 0 - ): # TODO: fix newline hack + ): if text_properties.compensate_curvature and glyph_advance > 0: previous_location, psi = calc_point_on_bezier_curve( mom, advance, False, True @@ -1027,67 +1114,46 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) output_spline_index=True, ) - ob.scale = (scalor, scalor, scalor) - advance = advance + glyph_advance + curve_compensation + glyph_index += 1 previous_spline_index = spline_index - if regenerate: - glyph_data = text_properties.glyphs.add() - glyph_data.glyph_id = glyph_id - glyph_data.glyph_object = ob - glyph_data.letter_spacing = 0 - - if regenerate: - mom[f"{utils.prefix()}_type"] = "textobject" - mom[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id - mom[f"{utils.prefix()}_font_name"] = text_properties.font_name - mom[f"{utils.prefix()}_face_name"] = text_properties.face_name - mom[f"{utils.prefix()}_font_size"] = text_properties.font_size - mom[f"{utils.prefix()}_letter_spacing"] = text_properties.letter_spacing - mom[f"{utils.prefix()}_orientation"] = text_properties.orientation - mom[f"{utils.prefix()}_translation"] = text_properties.translation - - if lock_depsgraph_update_n_times < 0: - lock_depsgraph_update_n_times = len( - bpy.context.selected_objects - ) - else: - lock_depsgraph_update_n_times += len( - bpy.context.selected_objects - ) - - # NOTE: we reset with a timeout, as setting and resetting certain things - # in fast succession will cause visual glitches (e.g. {}.data.use_path). - def reset(): - mom.data.use_path = previous_use_path - if counted_reset in bpy.app.handlers.depsgraph_update_post: - bpy.app.handlers.depsgraph_update_post.remove(counted_reset) - if bpy.app.timers.is_registered(reset): - bpy.app.timers.unregister(reset) - - molotov = reset_depsgraph_n + 0 - - def counted_reset(scene, depsgraph): - nonlocal molotov - if molotov == 0: - reset() - else: - molotov -= 1 - - # unregister previous resets to avoid multiple execution - if bpy.app.timers.is_registered(reset): - bpy.app.timers.unregister(reset) - if counted_reset in bpy.app.handlers.depsgraph_update_post: - bpy.app.handlers.depsgraph_update_post.remove(counted_reset) - - if not isinstance(reset_timeout_s, bool): - if reset_timeout_s > 0: - bpy.app.timers.register(reset, first_interval=reset_timeout_s) - elif reset_timeout <= 0: - reset() - - bpy.app.handlers.depsgraph_update_post.append(counted_reset) + # NOTE: depsgraph update not locked + # as we fixed data_path with parent_to_curve trick + # if lock_depsgraph_update_n_times < 0: + # lock_depsgraph_update_n_times = len( + # bpy.context.selected_objects + # ) + # else: + # lock_depsgraph_update_n_times += len( + # bpy.context.selected_objects + # ) + # # NOTE: we reset with a timeout, as setting and resetting certain things + # # in fast succession will cause visual glitches (e.g. {}.data.use_path). + # def reset(): + # mom.data.use_path = previous_use_path + # if counted_reset in bpy.app.handlers.depsgraph_update_post: + # bpy.app.handlers.depsgraph_update_post.remove(counted_reset) + # if bpy.app.timers.is_registered(reset): + # bpy.app.timers.unregister(reset) + # molotov = reset_depsgraph_n + 0 + # def counted_reset(scene, depsgraph): + # nonlocal molotov + # if molotov == 0: + # reset() + # else: + # molotov -= 1 + # # unregister previous resets to avoid multiple execution + # if bpy.app.timers.is_registered(reset): + # bpy.app.timers.unregister(reset) + # if counted_reset in bpy.app.handlers.depsgraph_update_post: + # bpy.app.handlers.depsgraph_update_post.remove(counted_reset) + # if not isinstance(reset_timeout_s, bool): + # if reset_timeout_s > 0: + # bpy.app.timers.register(reset, first_interval=reset_timeout_s) + # elif reset_timeout <= 0: + # reset() + # bpy.app.handlers.depsgraph_update_post.append(counted_reset) # endtime = time.perf_counter_ns() # elapsedtime = endtime - starttime @@ -1504,3 +1570,109 @@ 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 + if len(objects) == 0: + return "no objects selected" + + if bpy.context.active_object is None: + return "no active object selected" + + reference_origin_position = bpy.context.active_object.matrix_world.translation[axis] + + # do it + for o in objects: + is_possibly_glyph = is_glyph(o) + if is_possibly_glyph and o is not bpy.context.active_object: + if is_mesh(o): + diff = reference_origin_position - o.matrix_world.translation[axis] + + for v in o.data.vertices: + 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)]) + +# 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 + +# 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 + + +# 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" + + # 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" + + # 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] + + # for v in o.data.vertices: + # v.co[0] -= diff + + # o.location += mathutils.Vector((diff, 0.0, 0.0)) @ o.matrix_world.inverted() + + # for c in o.children: + # if is_metrics_object(c): + # c.location -= mathutils.Vector((diff, 0.0, 0.0)) @ o.matrix_world.inverted() + + # return "" From 7c9a72533828f1cb611064c77836e956348858ad Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sun, 18 May 2025 17:35:58 +0200 Subject: [PATCH 055/103] better comments --- __init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__init__.py b/__init__.py index 8666c7a..0a590b8 100644 --- a/__init__.py +++ b/__init__.py @@ -22,7 +22,7 @@ bl_info = { "description": "Convenience addon for 3D fonts", "category": "Typography", } -# NOTE: also change version in common/utils.py +# NOTE: also change version in common/utils.py and README.md # make sure that modules are reloadable # when registering From f911f2f23a9505c2bf9ee5721f23553bea4ef733 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sun, 18 May 2025 17:36:16 +0200 Subject: [PATCH 056/103] remove local vimrc --- _vimrc_local.vim | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 _vimrc_local.vim diff --git a/_vimrc_local.vim b/_vimrc_local.vim deleted file mode 100644 index cac1787..0000000 --- a/_vimrc_local.vim +++ /dev/null @@ -1,20 +0,0 @@ -""""""""""""""""""""""""""""""""" JEDI - -let g:jedi#auto_initialization = 1 -let g:jedi#use_tabs_not_buffers = 1 -let g:jedi#environment_path = "venv" - -""""""""""""""""""""""""""""""""" ALE - -"let g:ale_python_pylint_executable = '/home/jrkb/git/pointer/neomatter/font3d/abc3d/venv/bin/pylint' -"let g:ale_python_executable='/home/jrkb/git/pointer/neomatter/font3d/abc3d/venv/bin/python' -"let g:ale_python_pylint_use_global=1 -"let g:ale_use_global_executables=1 -"let g:ale_python_auto_pipenv=1 -"let g:ale_python_auto_virtualenv=1 -"let g:ale_virtualenv_dir_names = ['venv'] - -"let g:ale_linters = { 'javascript': ['eslint', 'tsserver'], 'python': ['jedils', 'pylint', 'flake8'], 'cpp': ['cc', 'clangcheck', 'clangd', 'clangtidy', 'clazy', 'cppcheck', 'cpplint', 'cquery', 'cspell', 'flawfinder'], 'php': ['php_cs_fixer'] } -"let g:ale_fixers = { '*': ['remove_trailing_lines', 'trim_whitespace'], 'python': ['autopep8'], 'cpp': ['uncrustify'], 'javascript': js_fixers, 'css': ['prettier'], 'json': ['prettier'], 'php': ['php_cs_fixer'] } - -"let g:ale_pattern_options = {'\.py$': {'ale_enabled': 0}} From 8094e56e5af76060592d7d945655afe8d9549933 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sun, 18 May 2025 17:36:54 +0200 Subject: [PATCH 057/103] cleanup --- common/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/common/utils.py b/common/utils.py index 0c99dfd..ffbc5ee 100644 --- a/common/utils.py +++ b/common/utils.py @@ -22,7 +22,6 @@ def prefix(): import datetime import time -from mathutils import Vector def get_timestamp(): From 56afa0b453b0bf2b0fbdc1f2237b6db79923f6db Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sun, 18 May 2025 17:37:04 +0200 Subject: [PATCH 058/103] bump version --- README.md | 2 +- __init__.py | 2 +- common/utils.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0b0bdee..e38ad46 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ / ___ \| |_) | |___ ___) | |_| | /_/ \_\____/ \____|____/|____/ ``` -v0.0.5 +v0.0.6 Convenience tool to work with 3D typography in Blender and Cinema4D. diff --git a/__init__.py b/__init__.py index 0a590b8..2a71c8f 100644 --- a/__init__.py +++ b/__init__.py @@ -16,7 +16,7 @@ from .common import Font, utils bl_info = { "name": "ABC3D", "author": "Jakob Schlötter, Studio Pointer*", - "version": (0, 0, 5), + "version": (0, 0, 6), "blender": (4, 1, 0), "location": "VIEW3D", "description": "Convenience addon for 3D fonts", diff --git a/common/utils.py b/common/utils.py index ffbc5ee..f2fe8d0 100644 --- a/common/utils.py +++ b/common/utils.py @@ -8,7 +8,7 @@ def get_version_minor(): def get_version_patch(): - return 5 + return 6 def get_version_string(): From 093d0813af5388eb91b850fd7764df71d3c724bc Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sun, 18 May 2025 18:46:07 +0200 Subject: [PATCH 059/103] fix selection being updated --- butils.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/butils.py b/butils.py index 94ed804..2e22b05 100644 --- a/butils.py +++ b/butils.py @@ -820,11 +820,12 @@ def update_matrices(obj): def is_or_has_parent(o, parent, if_is_parent=True, max_depth=10): if o == parent and if_is_parent: return True + oo = o for i in range(0, max_depth): - o = o.parent - if o == parent: + oo = oo.parent + if oo == parent: return True - if o is None: + if oo is None: return False return False From 6c3ad47cb6f869d9fcb4e6ce35ba24df821ef943 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sun, 18 May 2025 18:46:20 +0200 Subject: [PATCH 060/103] remove print --- butils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/butils.py b/butils.py index 2e22b05..fbb48b0 100644 --- a/butils.py +++ b/butils.py @@ -1074,7 +1074,6 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) ############### PREPARE FOR THE NEXT - print(f"{glyph_id}: {glyph_properties.letter_spacing=}") glyph_advance = ( glyph_post_advance * scalor + text_properties.letter_spacing + glyph_properties.letter_spacing ) From ca8b4302a3c7e9cc06a0d890f667c023682c7eff Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sun, 18 May 2025 18:46:35 +0200 Subject: [PATCH 061/103] fix accessing None --- __init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__init__.py b/__init__.py index 2a71c8f..4b8a7ab 100644 --- a/__init__.py +++ b/__init__.py @@ -724,7 +724,7 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): if f"{utils.prefix()}_linked_textobject" in a_o: text_index = a_o[f"{utils.prefix()}_linked_textobject"] return bpy.context.scene.abc3d_data.available_texts[text_index] - elif f"{utils.prefix()}_linked_textobject" in a_o.parent: + elif a_o.parent is not None and f"{utils.prefix()}_linked_textobject" in a_o.parent: text_index = a_o.parent[f"{utils.prefix()}_linked_textobject"] return bpy.context.scene.abc3d_data.available_texts[text_index] else: From 88f5579d40e31407e997bca36a6a9945603a1313 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sun, 18 May 2025 18:47:00 +0200 Subject: [PATCH 062/103] loop in and out --- __init__.py | 14 ++++++++++++++ butils.py | 14 ++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/__init__.py b/__init__.py index 4b8a7ab..ef361dc 100644 --- a/__init__.py +++ b/__init__.py @@ -242,6 +242,18 @@ class ABC3D_text_properties(bpy.types.PropertyGroup): name="Ignore Curve Orientation", default=False, ) + loop_in: bpy.props.BoolProperty( + update=update_callback, + name="Loop In", + description="Loop letter on curve if negative offset would place it in front of it.", + default=False, + ) + loop_out: bpy.props.BoolProperty( + update=update_callback, + name="Loop Out", + description="Loop letter on curve if a large offset would place it behind it.", + default=False, + ) distribution_type: bpy.props.StringProperty() glyphs: bpy.props.CollectionProperty(type=ABC3D_glyph_properties) @@ -809,6 +821,8 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): layout.row().prop(props, "offset") layout.row().prop(props, "compensate_curvature") layout.row().prop(props, "ignore_orientation") + layout.row().prop(props, "loop_in") + layout.row().prop(props, "loop_out") layout.column().prop(props, "translation") layout.column().prop(props, "orientation") diff --git a/butils.py b/butils.py index fbb48b0..eec7106 100644 --- a/butils.py +++ b/butils.py @@ -1014,11 +1014,21 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) glyph_pre_advance, glyph_post_advance = get_glyph_prepost_advances(glyph) advance += glyph_pre_advance * scalor + # check if we want to loop + applied_advance = advance + if text_properties.loop_in: + if applied_advance < 0: + applied_advance %= curve_length + + if text_properties.loop_out: + if applied_advance > curve_length: + applied_advance %= curve_length + if distribution_type == "FOLLOW_PATH": 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 = 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" @@ -1034,7 +1044,7 @@ 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, 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: From d6dfbfa5a13853702305f00ec9e4a03dc8aa18d9 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Tue, 20 May 2025 19:21:32 +0200 Subject: [PATCH 063/103] cleanup --- butils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/butils.py b/butils.py index eec7106..c87b10d 100644 --- a/butils.py +++ b/butils.py @@ -634,7 +634,6 @@ def ShowMessageBox(title="Message Box", icon="INFO", message="", prevent_repeat= if prevent_repeat: for m in message_memory: if m[0] == title and m[1] == icon and m[2] == message: - print("PREVENT PREVENT") return message_memory.append([title, icon, message]) myLines = message From 2422d0cf0978f1575c70d6dda63df6efba3a99e6 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Tue, 20 May 2025 19:22:00 +0200 Subject: [PATCH 064/103] clean startup --- __init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/__init__.py b/__init__.py index ef361dc..33d293b 100644 --- a/__init__.py +++ b/__init__.py @@ -1829,9 +1829,10 @@ def load_used_glyphs(): 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.register_installed_fonts) butils.run_in_main_thread(butils.update_available_fonts) - butils.run_in_main_thread(bpy.ops.abc3d.load_installed_fonts) butils.run_in_main_thread(load_used_glyphs) + butils.run_in_main_thread(butils.update_types) def load_handler_unload(): @@ -1917,12 +1918,11 @@ def register(): bpy.app.handlers.depsgraph_update_post.append(on_depsgraph_update) butils.run_in_main_thread(butils.clear_available_fonts) - # butils.run_in_main_thread(butils.load_installed_fonts) + butils.run_in_main_thread(butils.register_installed_fonts) butils.run_in_main_thread(butils.update_available_fonts) + butils.run_in_main_thread(load_used_glyphs) butils.run_in_main_thread(butils.update_types) - # bpy.ops.abc3d.load_installed_fonts() - Font.init() From 7ebe913e49c4a24ff7607cddcc8e2f140d8e2603 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Tue, 20 May 2025 19:24:43 +0200 Subject: [PATCH 065/103] fix rendering crashes 1) introduce can_regenerate so we only regenerate when necessary 2) no notifications of missing glyphs when rendering 3) use frame_change_pre instead of post --- __init__.py | 14 +++++++------- butils.py | 21 +++++++++++---------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/__init__.py b/__init__.py index 33d293b..8d54e7e 100644 --- a/__init__.py +++ b/__init__.py @@ -180,7 +180,7 @@ class ABC3D_text_properties(bpy.types.PropertyGroup): def glyphs_update_callback(self, context): butils.prepare_text(self.font_name, self.face_name, self.text) - butils.set_text_on_curve(self) + butils.set_text_on_curve(self, can_regenerate=True) def update_callback(self, context): butils.set_text_on_curve(self) @@ -1321,8 +1321,8 @@ class ABC3D_OT_PlaceText(bpy.types.Operator): # t['font'] = self.font # enums want to be set as attribute t["font_name"] = self.font_name t["face_name"] = self.face_name - t.text_object = selected - t["text"] = self.text + t["text_object"] = selected + t.text = self.text t["letter_spacing"] = self.letter_spacing t["font_size"] = self.font_size t["offset"] = self.offset @@ -1911,8 +1911,8 @@ def register(): # and autostart if we reload script load_handler(None, None) - if on_frame_changed not in bpy.app.handlers.frame_change_post: - bpy.app.handlers.frame_change_post.append(on_frame_changed) + if on_frame_changed not in bpy.app.handlers.frame_change_pre: + bpy.app.handlers.frame_change_pre.append(on_frame_changed) if on_depsgraph_update not in bpy.app.handlers.depsgraph_update_post: bpy.app.handlers.depsgraph_update_post.append(on_depsgraph_update) @@ -1938,8 +1938,8 @@ def unregister(): # and when reload script load_handler_unload() - if on_frame_changed in bpy.app.handlers.frame_change_post: - bpy.app.handlers.frame_change_post.remove(on_frame_changed) + if on_frame_changed in bpy.app.handlers.frame_change_pre: + bpy.app.handlers.frame_change_pre.remove(on_frame_changed) if on_depsgraph_update in bpy.app.handlers.depsgraph_update_post: bpy.app.handlers.depsgraph_update_post.remove(on_depsgraph_update) diff --git a/butils.py b/butils.py index c87b10d..9b696d1 100644 --- a/butils.py +++ b/butils.py @@ -777,7 +777,7 @@ def is_bezier(curve): return True -def will_regenerate(text_properties): +def would_regenerate(text_properties): mom = text_properties.text_object if len(text_properties.text) != len(text_properties.glyphs): @@ -843,7 +843,7 @@ def parent_to_curve(o, c): 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): +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. @@ -879,7 +879,7 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) # elif distribution_type == "FOLLOW_PATH": # mom.data.use_path = True - regenerate = will_regenerate(text_properties) + regenerate = can_regenerate and would_regenerate(text_properties) # if we regenerate.... delete objects if regenerate and text_properties.get("glyphs"): @@ -954,12 +954,13 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) message = message + f" (replaced with '{possible_replacement}')" replaced = True - ShowMessageBox( - title="Glyph replaced" if replaced else "Glyph missing", - icon="INFO" if replaced else "ERROR", - message=message, - prevent_repeat=True, - ) + if can_regenerate: + ShowMessageBox( + title="Glyph replaced" if replaced else "Glyph missing", + icon="INFO" if replaced else "ERROR", + message=message, + prevent_repeat=True, + ) if not replaced: continue @@ -997,6 +998,7 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) inner_node.parent = outer_node inner_node.matrix_parent_inverse = outer_node.matrix_world.inverted() parent_to_curve(outer_node, mom) + outer_node.hide_set(True) glyph_properties["glyph_object"] = outer_node else: @@ -1079,7 +1081,6 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) inner_node.rotation_mode = previous_inner_node_rotation_mode # outer_node.hide_viewport = True - outer_node.hide_set(True) ############### PREPARE FOR THE NEXT From 49699db30983a49c6f29076b3b08d0f1ed8bdd58 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Fri, 23 May 2025 11:33:38 +0200 Subject: [PATCH 066/103] regenerate if needed in update_callback --- __init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/__init__.py b/__init__.py index 8d54e7e..f1d8157 100644 --- a/__init__.py +++ b/__init__.py @@ -183,7 +183,10 @@ class ABC3D_text_properties(bpy.types.PropertyGroup): butils.set_text_on_curve(self, can_regenerate=True) def update_callback(self, context): - butils.set_text_on_curve(self) + try: + butils.set_text_on_curve(self) + except (AttributeError, TypeError): + butils.set_text_on_curve(self, can_regenerate=True) def font_update_callback(self, context): font_name, face_name = self.font.split(" ") From 13b5a4dd88a946b22a92537e9579ac3c5df98a7e Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Fri, 23 May 2025 11:37:54 +0200 Subject: [PATCH 067/103] bump version to v0.0.7 --- README.md | 2 +- __init__.py | 2 +- common/utils.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e38ad46..ae7cf9a 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ / ___ \| |_) | |___ ___) | |_| | /_/ \_\____/ \____|____/|____/ ``` -v0.0.6 +v0.0.7 Convenience tool to work with 3D typography in Blender and Cinema4D. diff --git a/__init__.py b/__init__.py index f1d8157..b866537 100644 --- a/__init__.py +++ b/__init__.py @@ -16,7 +16,7 @@ from .common import Font, utils bl_info = { "name": "ABC3D", "author": "Jakob Schlötter, Studio Pointer*", - "version": (0, 0, 6), + "version": (0, 0, 7), "blender": (4, 1, 0), "location": "VIEW3D", "description": "Convenience addon for 3D fonts", diff --git a/common/utils.py b/common/utils.py index f2fe8d0..8219a94 100644 --- a/common/utils.py +++ b/common/utils.py @@ -8,7 +8,7 @@ def get_version_minor(): def get_version_patch(): - return 6 + return 7 def get_version_string(): From e21ecaef0a18a59a44e84480d75b602401e786ad Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sat, 24 May 2025 14:54:24 +0200 Subject: [PATCH 068/103] fix blender import path 4.3+ --- bimport.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/bimport.py b/bimport.py index 27cd8b5..cad66de 100644 --- a/bimport.py +++ b/bimport.py @@ -4,11 +4,10 @@ from bpy.props import ( BoolProperty, EnumProperty, IntProperty, - FloatProperty, CollectionProperty, ) from bpy.types import Operator -from bpy_extras.io_utils import ImportHelper, ExportHelper +from bpy_extras.io_utils import ImportHelper from io_scene_gltf2 import ConvertGLTF2_Base import importlib @@ -16,7 +15,7 @@ import importlib if "Font" in locals(): importlib.reload(Font) else: - from .common import Font + pass if "utils" in locals(): importlib.reload(utils) @@ -50,7 +49,7 @@ def get_font_faces_in_file(filepath): out.append(node.extras) return out - except ImportError as e: + except ImportError: return None @@ -60,7 +59,7 @@ def get_font_faces_in_file(filepath): 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_idname = "abc3d.check_font_gltf" bl_label = "Check glTF 2.0 Font" bl_options = {"REGISTER", "UNDO"} @@ -77,7 +76,6 @@ class GetFontFacesInFile(Operator, ImportHelper): def check_gltf2(self, context): import os - import sys if self.files: # Multiple file check @@ -100,7 +98,7 @@ class GetFontFacesInFile(Operator, ImportHelper): class ImportGLTF2(Operator, ConvertGLTF2_Base, ImportHelper): """Load a glTF 2.0 font""" - bl_idname = f"abc3d.import_font_gltf" + bl_idname = "abc3d.import_font_gltf" bl_label = "Import glTF 2.0 Font" bl_options = {"REGISTER", "UNDO"} @@ -285,11 +283,18 @@ class ImportGLTF2(Operator, ConvertGLTF2_Base, ImportHelper): 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: + 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 + except (ModuleNotFoundError, ImportError): + from io_scene_gltf2.io.imp.blender_gltf import glTFImporter, ImportError + from io_scene_gltf2.blender.imp.blender_gltf import BlenderGlTF + from io_scene_gltf2.blender.imp.vnode import VNode, compute_vnodes + from io_scene_gltf2.blender.com.extras import set_extras + from io_scene_gltf2.blender.imp.node import BlenderNode try: gltf = glTFImporter(filename, import_settings) From 4113343e791296e12e3cceab15ba2feb0641f3e7 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sat, 24 May 2025 15:18:20 +0200 Subject: [PATCH 069/103] move imports to top --- bimport.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/bimport.py b/bimport.py index cad66de..85cadbe 100644 --- a/bimport.py +++ b/bimport.py @@ -22,13 +22,24 @@ if "utils" in locals(): else: from .common import utils +try: + 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 +except (ModuleNotFoundError, ImportError): + from io_scene_gltf2.io.imp.gltf2_io_gltf import glTFImporter, ImportError + from io_scene_gltf2.blender.imp.blender_gltf import BlenderGlTF + from io_scene_gltf2.blender.imp.vnode import VNode, compute_vnodes + from io_scene_gltf2.blender.com.extras import set_extras + from io_scene_gltf2.blender.imp.node import BlenderNode + # 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) @@ -283,18 +294,6 @@ class ImportGLTF2(Operator, ConvertGLTF2_Base, ImportHelper): def unit_import(self, filename, import_settings): import time - try: - 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 - except (ModuleNotFoundError, ImportError): - from io_scene_gltf2.io.imp.blender_gltf import glTFImporter, ImportError - from io_scene_gltf2.blender.imp.blender_gltf import BlenderGlTF - from io_scene_gltf2.blender.imp.vnode import VNode, compute_vnodes - from io_scene_gltf2.blender.com.extras import set_extras - from io_scene_gltf2.blender.imp.node import BlenderNode try: gltf = glTFImporter(filename, import_settings) From 7b4e65cbb7fc463307019faa1ee835a717eb0777 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sat, 24 May 2025 15:22:01 +0200 Subject: [PATCH 070/103] check for None --- butils.py | 10 +++++----- common/Font.py | 14 +++++++------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/butils.py b/butils.py index 9b696d1..6663a1a 100644 --- a/butils.py +++ b/butils.py @@ -358,7 +358,7 @@ def turn_collection_hierarchy_into_path(obj): def find_font_object(fontcollection, font_name): fonts = find_objects_by_custom_property(fontcollection.objects, "is_font", True) for font in fonts: - if font["font_name"] == font_name and font.parent == None: + if font["font_name"] == font_name and font.parent is None: return font return None @@ -375,7 +375,7 @@ def move_in_fontcollection(obj, fontcollection, allow_duplicates=False): # parent nesting structure # the font object font_obj = find_font_object(fontcollection, obj["font_name"]) - if font_obj == None: + if font_obj is None: font_obj = bpy.data.objects.new(obj["font_name"], None) font_obj.empty_display_type = "PLAIN_AXES" fontcollection.objects.link(font_obj) @@ -386,7 +386,7 @@ def move_in_fontcollection(obj, fontcollection, allow_duplicates=False): # the face object as a child of font object face_obj = find_font_face_object(font_obj, obj["face_name"]) - if face_obj == None: + if face_obj is None: face_obj = bpy.data.objects.new(obj["face_name"], None) face_obj.empty_display_type = "PLAIN_AXES" face_obj["is_face"] = True @@ -678,8 +678,8 @@ def is_metrics_object(o): if f"{utils.prefix()}_type" in o: return o[f"{utils.prefix()}_type"] == "metrics" return ( - re.match(".*_metrics$", o.name) != None - or re.match(".*_metrics.[\d]{3}$", o.name) != None + re.match(".*_metrics$", o.name) is not None + or re.match(".*_metrics.[\d]{3}$", o.name) is not None ) and is_mesh(o) diff --git a/common/Font.py b/common/Font.py index bcb7949..e7a7ac0 100644 --- a/common/Font.py +++ b/common/Font.py @@ -163,7 +163,7 @@ class Font: 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: + if fonts[font_name].faces.get(face_name) is None: fonts[font_name].faces[face_name] = FontFace({}) fonts[font_name].faces[face_name].glyphs_in_fontfile = glyphs_in_fontfile else: @@ -193,9 +193,9 @@ def add_glyph(font_name, face_name, glyph_id, glyph_object): if not fonts.keys().__contains__(font_name): fonts[font_name] = Font({}) - if fonts[font_name].faces.get(face_name) == None: + if fonts[font_name].faces.get(face_name) is None: fonts[font_name].faces[face_name] = FontFace({}) - if fonts[font_name].faces[face_name].glyphs.get(glyph_id) == None: + if fonts[font_name].faces[face_name].glyphs.get(glyph_id) is None: fonts[font_name].faces[face_name].glyphs[glyph_id] = [] fonts[font_name].faces[face_name].glyphs.get(glyph_id).append(glyph_object) @@ -224,13 +224,13 @@ def get_glyph(font_name, face_name, glyph_id, alternate=0): return None face = fonts[font_name].faces.get(face_name) - if face == None: + 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 glyphs_for_id = face.glyphs.get(glyph_id) - if glyphs_for_id == None or len(glyphs_for_id) <= alternate: + 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) @@ -243,7 +243,7 @@ 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 + or fonts[font_name].faces.get(face_name) is None ): return "", "", text # , , @@ -286,7 +286,7 @@ MISSING_FACE = 1 def test_availability(font_name, face_name, text): if not fonts.keys().__contains__(font_name): return MISSING_FONT - if fonts[font_name].faces.get(face_name) == None: + if fonts[font_name].faces.get(face_name) is None: return MISSING_FACE loaded, missing, maybe, filepaths = test_glyphs_availability( font_name, face_name, text From 840fdf1ca40425a4c2d767f2c5b6579ad9a156af Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sat, 24 May 2025 15:25:12 +0200 Subject: [PATCH 071/103] bump version to v0.0.8 --- README.md | 2 +- __init__.py | 2 +- common/utils.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ae7cf9a..c3c8ddc 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ / ___ \| |_) | |___ ___) | |_| | /_/ \_\____/ \____|____/|____/ ``` -v0.0.7 +v0.0.8 Convenience tool to work with 3D typography in Blender and Cinema4D. diff --git a/__init__.py b/__init__.py index b866537..9dc72d6 100644 --- a/__init__.py +++ b/__init__.py @@ -16,7 +16,7 @@ from .common import Font, utils bl_info = { "name": "ABC3D", "author": "Jakob Schlötter, Studio Pointer*", - "version": (0, 0, 7), + "version": (0, 0, 8), "blender": (4, 1, 0), "location": "VIEW3D", "description": "Convenience addon for 3D fonts", diff --git a/common/utils.py b/common/utils.py index 8219a94..0605d60 100644 --- a/common/utils.py +++ b/common/utils.py @@ -8,7 +8,7 @@ def get_version_minor(): def get_version_patch(): - return 7 + return 8 def get_version_string(): From 3ea2f0e30445dbdb967bc916c20c4fd8adfd0989 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sun, 25 May 2025 14:15:21 +0200 Subject: [PATCH 072/103] ignore more venvs --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 220a137..19bc9a2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # python __pycache__ venv +venv* # vim *.swo From 7a034efd1c61cb75dc91f8b044421d4f43612ac9 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sun, 25 May 2025 14:15:42 +0200 Subject: [PATCH 073/103] all floats --- common/spacesUnicode.txt | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/common/spacesUnicode.txt b/common/spacesUnicode.txt index da6a7c9..b7270e0 100644 --- a/common/spacesUnicode.txt +++ b/common/spacesUnicode.txt @@ -4,20 +4,20 @@ space 0020 0.25 nbspace 00A0 0.25 # ethi:wordspace 1361 # NOTE: has shape enquad 2000 0.5 -emquad 2001 1 +emquad 2001 1.0 enspace 2002 0.5 -emspace 2003 1 -threeperemspace 2004 3 -fourperemspace 2005 4 -sixperemspace 2006 6 -figurespace 2007 1 -punctuationspace 2008 1 +emspace 2003 1.0 +threeperemspace 2004 3.0 +fourperemspace 2005 4.0 +sixperemspace 2006 6.0 +figurespace 2007 1.0 +punctuationspace 2008 1.0 thinspace 2009 0.1 hairspace 200A 0.05 -zerowidthspace 200B 0 +zerowidthspace 200B 0.0 narrownobreakspace 202F 0.1 -mediummathematicalspace 205F 1 +mediummathematicalspace 205F 1.0 cntr:space 2420 0.25 -ideographicspace 3000 1 +ideographicspace 3000 1.0 # ideographichalffillspace 303F # NOTE: has shape -zerowidthnobreakspace FEFF 0 +zerowidthnobreakspace FEFF 0.0 From c27cf41368adca737208a681106c9f5443073560 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sun, 25 May 2025 14:16:15 +0200 Subject: [PATCH 074/103] introduce detect_text() and friends --- __init__.py | 192 +++++++++++++++++--------- butils.py | 378 ++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 423 insertions(+), 147 deletions(-) diff --git a/__init__.py b/__init__.py index 9dc72d6..d1695de 100644 --- a/__init__.py +++ b/__init__.py @@ -451,7 +451,7 @@ class ABC3D_PT_TextPlacement(bpy.types.Panel): @classmethod def poll(self, context): if ( - type(context.active_object) != type(None) + context.active_object is not None and context.active_object.type == "CURVE" ): self.can_place = True @@ -502,8 +502,8 @@ class ABC3D_PT_TextManagement(bpy.types.Panel): for c in t.text_object.children: if ( len(c.users_collection) > 0 - and not isinstance(c.get(f"{utils.prefix()}_linked_textobject"), None) - and c.get(f"{utils.prefix()}_linked_textobject") == t.text_id + and not isinstance(c.get(f"{utils.prefix()}_text_id"), None) + and c.get(f"{utils.prefix()}_text_id") == t.text_id ): remove_me = False # not sure how to solve this reliably atm, @@ -542,14 +542,14 @@ class ABC3D_PT_TextManagement(bpy.types.Panel): remove_list.append(i) for i in remove_list: - if type(abc3d_data.available_texts[i].text_object) != type(None): + if abc3d_data.available_texts[i].text_object is not None: mom = abc3d_data.available_texts[i].text_object def delif(o, p): if p in o: del o[p] - delif(mom, f"{utils.prefix()}_linked_textobject") + delif(mom, f"{utils.prefix()}_text_id") delif(mom, f"{utils.prefix()}_font_name") delif(mom, f"{utils.prefix()}_face_name") delif(mom, f"{utils.prefix()}_font_size") @@ -736,11 +736,11 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): # and bpy.context.object.select_get(): a_o = bpy.context.active_object if a_o is not None: - if f"{utils.prefix()}_linked_textobject" in a_o: - text_index = a_o[f"{utils.prefix()}_linked_textobject"] + if f"{utils.prefix()}_text_id" in a_o: + text_index = a_o[f"{utils.prefix()}_text_id"] return bpy.context.scene.abc3d_data.available_texts[text_index] - elif a_o.parent is not None and f"{utils.prefix()}_linked_textobject" in a_o.parent: - text_index = a_o.parent[f"{utils.prefix()}_linked_textobject"] + elif a_o.parent is not None and f"{utils.prefix()}_text_id" in a_o.parent: + text_index = a_o.parent[f"{utils.prefix()}_text_id"] return bpy.context.scene.abc3d_data.available_texts[text_index] else: for t in bpy.context.scene.abc3d_data.available_texts: @@ -752,9 +752,9 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): def get_active_glyph_properties(self): a_o = bpy.context.active_object if a_o is not None: - if (f"{utils.prefix()}_linked_textobject" 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()}_linked_textobject"] + 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] else: @@ -795,13 +795,13 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): @classmethod def poll(self, context): - return self.get_active_text_properties(self) is not None + try: + return self.get_active_text_properties(self) is not None + except IndexError: + return False def draw(self, context): layout = self.layout - wm = context.window_manager - scene = context.scene - abc3d_data = scene.abc3d_data props = self.get_active_text_properties() glyph_props = self.get_active_glyph_properties() @@ -1190,6 +1190,11 @@ class ABC3D_OT_RemoveText(bpy.types.Operator): description="Remove both ABC3D text functionality and the objects/meshes", default=True, ) + remove_custom_properties: bpy.props.BoolProperty( + name="Remove Custom Properties", + description="Remove ABC3D custom properties of objects", + default=True, + ) def invoke(self, context, event): wm = context.window_manager @@ -1206,26 +1211,27 @@ class ABC3D_OT_RemoveText(bpy.types.Operator): return {"CANCELLED"} i = abc3d_data.active_text_index - if type(abc3d_data.available_texts[i].text_object) != type(None): + if abc3d_data.available_texts[i].text_object is not None: mom = abc3d_data.available_texts[i].text_object - def delif(o, p): - if p in o: - del o[p] + if self.remove_custom_properties: + def delif(o, p): + if p in o: + del o[p] - delif(mom, f"{utils.prefix()}_type") - delif(mom, f"{utils.prefix()}_linked_textobject") - delif(mom, f"{utils.prefix()}_font_name") - delif(mom, f"{utils.prefix()}_face_name") - delif(mom, f"{utils.prefix()}_font_size") - delif(mom, f"{utils.prefix()}_letter_spacing") - delif(mom, f"{utils.prefix()}_orientation") - delif(mom, f"{utils.prefix()}_translation") - delif(mom, f"{utils.prefix()}_offset") + delif(mom, f"{utils.prefix()}_type") + delif(mom, f"{utils.prefix()}_text_id") + delif(mom, f"{utils.prefix()}_font_name") + delif(mom, f"{utils.prefix()}_face_name") + delif(mom, f"{utils.prefix()}_font_size") + delif(mom, f"{utils.prefix()}_letter_spacing") + delif(mom, f"{utils.prefix()}_orientation") + delif(mom, f"{utils.prefix()}_translation") + delif(mom, f"{utils.prefix()}_offset") if self.remove_objects: remove_list = [] for g in abc3d_data.available_texts[i].glyphs: - if type(g) != type(None): + if g is not None: remove_list.append(g.glyph_object) butils.simply_delete_objects(remove_list) @@ -1787,18 +1793,61 @@ def compare_text_object_with_object(t, o, strict=False): def detect_text(): + lock_depsgraph_updates(auto_unlock_s=-1) + print("DETECT TEXT:: begin") scene = bpy.context.scene abc3d_data = scene.abc3d_data - for o in scene.objects: - if o[f"{utils.prefix()}_type"] == "textobject": - linked_textobject = int(o[f"{utils.prefix()}_linked_textobject"]) + required_keys = [ + "type", + "text_id", + "font_name", + "face_name", + "text", + ] + objects = scene.objects + for o in objects: + print(f" {o.name=}") + valid = True + for key in required_keys: + if butils.get_key(key) not in o: + # print(f" key {butils.get_key(key)} not there") + valid = False + break + if not valid: + continue + print(" object may be valid textobject") + if o[butils.get_key("type")] == "textobject": + print(" {o.name=} a textobject") + print(f" {type(o)=} {o.name=}") + current_text_id = int(o[butils.get_key("text_id")]) if ( - len(abc3d_data.available_texts) > linked_textobject - and abc3d_data.available_texts[linked_textobject].text_object == o + len(abc3d_data.available_texts) > current_text_id + and abc3d_data.available_texts[current_text_id].text_object == o ): - t = abc3d_data.available_texts[linked_textobject] - a = test_availability(o["font_name"], o["face_name"], o["text"]) - butils.transfer_blender_object_to_text_properties(o, t) + print(" {o.name=} seems fine") + pass + # t = abc3d_data.available_texts[text_id] + # a = test_availability(o[butils.get_key("font_name")], + # o[butils.get_key("face_name")], + # o[butils.get_key("text")]) + # butils.transfer_text_object_to_text_properties(o, t) + else: + butils.link_text_object_with_new_text_properties(o, scene) + # print(" {o.name=} is a duplicate") + # text_id = butils.find_free_text_id() + # t = abc3d_data.available_texts.add() + # t["text_id"] = text_id + # print(f" found free {text_id=}") + # print(" preparing text") + # butils.prepare_text(o[butils.get_key("font_name")], + # o[butils.get_key("face_name")], + # o[butils.get_key("text")]) + # print(" prepared text, transferring text object") + # t.text_object = o + # butils.transfer_text_object_to_text_properties(o, t) + # print(" {o.name=} transerred text object") + print("DETECT TEXT:: end") + unlock_depsgraph_updates() def load_used_glyphs(): @@ -1850,21 +1899,25 @@ def on_frame_changed(self, dummy): butils.set_text_on_curve(t) -depsgraph_updates_locked = False +depsgraph_updates_locked = 0 def unlock_depsgraph_updates(): global depsgraph_updates_locked - depsgraph_updates_locked = False + depsgraph_updates_locked -= 1 -def lock_depsgraph_updates(): +def lock_depsgraph_updates(auto_unlock_s=1): global depsgraph_updates_locked - depsgraph_updates_locked = True - if bpy.app.timers.is_registered(unlock_depsgraph_updates): - bpy.app.timers.unregister(unlock_depsgraph_updates) - bpy.app.timers.register(unlock_depsgraph_updates, first_interval=1) + depsgraph_updates_locked += 1 + if auto_unlock_s >= 0: + if bpy.app.timers.is_registered(unlock_depsgraph_updates): + 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 @@ -1872,29 +1925,44 @@ import time @persistent def on_depsgraph_update(scene, depsgraph): global depsgraph_updates_locked - if not bpy.context.mode.startswith("EDIT") and not depsgraph_updates_locked: + print("DEPSGRAPH:: BEGIN") + if not bpy.context.mode.startswith("EDIT") and not are_depsgraph_updates_locked(): for u in depsgraph.updates: if ( - f"{utils.prefix()}_linked_textobject" in u.id.keys() - and f"{utils.prefix()}_type" in u.id.keys() - and u.id[f"{utils.prefix()}_type"] == "textobject" + butils.get_key("text_id") in u.id.keys() + and butils.get_key("type") in u.id.keys() + and u.id[butils.get_key("type")] == "textobject" ): - linked_textobject = u.id[f"{utils.prefix()}_linked_textobject"] - if ( - u.is_updated_geometry - and len(scene.abc3d_data.available_texts) > linked_textobject - ): - lock_depsgraph_updates() + print("DEPSGRAPH:: we have a textobject") + text_id = u.id[butils.get_key("text_id")] + if u.is_updated_geometry: + print(" updated geometry is true") + print(f" is {len(scene.abc3d_data.available_texts)} bigger than {text_id=} is true?") + # butils.detect_texts() + if len(scene.abc3d_data.available_texts) > text_id: + print(" YES") + print(" is ? text object is not u.id") + if scene.abc3d_data.available_texts[text_id].text_object != u.id: + print(" yes") + else: + print(" no") + else: + print(" NO") + print("DEPSGRAPH:: done textobject") + # lock_depsgraph_updates() - def later(): - if butils.lock_depsgraph_update_n_times <= 0: - butils.set_text_on_curve( - scene.abc3d_data.available_texts[linked_textobject] - ) - elif butils.lock_depsgraph_update_n_times <= 0: - butils.lock_depsgraph_update_n_times -= 1 + # def later(): + # if butils.lock_depsgraph_update_n_times <= 0: + # butils.set_text_on_curve( + # scene.abc3d_data.available_texts[text_id] + # ) + # elif butils.lock_depsgraph_update_n_times <= 0: + # butils.lock_depsgraph_update_n_times -= 1 - butils.run_in_main_thread(later) + # butils.run_in_main_thread(later) + if are_depsgraph_updates_locked(): + print(" L O C K E D") + print("DEPSGRAPH:: done") def register(): diff --git a/butils.py b/butils.py index 6663a1a..7c38fe5 100644 --- a/butils.py +++ b/butils.py @@ -52,6 +52,9 @@ def get_parent_collection_names(collection, parent_names): get_parent_collection_names(parent_collection, parent_names) return +def get_key(key): + return f"{utils.prefix()}_{key}" + # Ensure it's a curve object # TODO: no raising, please @@ -556,8 +559,8 @@ def update_available_fonts(): # def update_available_texts(): # abc3d_data = bpy.context.scene.abc3d_data # for o in bpy.context.scene.objects: -# if "linked_textobject" in o.keys(): -# i = o["linked_textobject"] +# if "text_id" in o.keys(): +# i = o["text_id"] # found = False # if len(abc3d_data.available_texts) > i: # if abc3d_data.available_texts[i].glyphs @@ -692,7 +695,7 @@ def is_text_object(o): return False -def is_glyph(o): +def is_glyph_object(o): if f"{utils.prefix()}_type" in o: return o[f"{utils.prefix()}_type"] == "glyph" try: @@ -706,6 +709,10 @@ def is_glyph(o): return False +def is_glyph(o): + return is_glyph_object(o) + + def update_types(): scene = bpy.context.scene abc3d_data = scene.abc3d_data @@ -777,9 +784,280 @@ def is_bezier(curve): return True -def would_regenerate(text_properties): - mom = text_properties.text_object +text_object_keys = [ + "font_name", + "face_name", + "type", + "text_id", + "font_size", + "letter_spacing", + "distribution_type", + "orientation", + "translation", + "offset", + "text", +] +glyph_object_keys = [ + "type", + "glyph_index", + "glyph_id", + "text_id", + "font_name", + "face_name", + "font_size", + "letter_spacing", + "alternate", +] + +ignore_keys_in_text_object_comparison = [ + "type", +] + +ignore_keys_in_glyph_object_comparison = [ + "type", + "glyph_index", + "font_name", + "face_name", + "text_id", +] + +ignore_keys_in_glyph_object_transfer = [ + "type", + "text_id", + "glyph_index", +] + +keys_trigger_regeneration = [ + "font_name", + "face_name", +] + +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 + text_id = 0 + found_free = False + while not found_free: + occupied = False + for t in abc3d_data.available_texts: + if text_id == t.text_id: + occupied = True + if occupied: + text_id += 1 + else: + 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_object_property = o[object_key] if object_key in o else False + if text_property != text_object_property: + if key in keys_trigger_regeneration: + print(f"{key}: REGENERATE {text_property=} {text_object_property}") + 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]): + print(f"{key}: DIFFER {text_property=} {text_object_property}") + return COMPARE_TEXT_OBJECT_DIFFER + else: + print(f"{key}: SAME {text_property.xyz=} {text_object_property.to_list()}") + else: + print(f"{key}: DIFFER {text_property=} {text_object_property}") + return COMPARE_TEXT_OBJECT_DIFFER + else: + print(f"{key}: SAME {text_property=} {text_object_property}") + return COMPARE_TEXT_OBJECT_SAME + + +def transfer_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) + 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) + if glyph_tmp is None: + space_width = Font.is_space(glyph_id) + if space_width: + return space_width + + message = f"Glyph not found for font_name='{text_properties.font_name}' face_name='{text_properties.face_name}' glyph_id='{glyph_id}'" + replaced = False + if glyph_id.isalpha(): + possible_replacement = glyph_id.swapcase() + glyph_tmp = Font.get_glyph( + text_properties.font_name, + text_properties.face_name, + possible_replacement, + -1 + ) + if glyph_tmp is not None: + message = message + f" (replaced with '{possible_replacement}')" + replaced = True + + if notify_on_replacement: + ShowMessageBox( + title="Glyph replaced" if replaced else "Glyph missing", + icon="INFO" if replaced else "ERROR", + message=message, + prevent_repeat=True, + ) + if not replaced: + return None + + return glyph_tmp.original + +def transfer_text_object_to_text_properties(o, text_properties, id_from_text_properties=True): + print("TRANSFER:: BEGIN") + print(f" {text_properties['text_id']=}") + print(f" {type(o)=}") + for key in text_object_keys: + if key in ignore_keys_in_text_object_comparison: + continue + object_key = get_key(key) + if id_from_text_properties and key == "text_id": + o[object_key] = text_properties["text_id"] + print(f" {object_key} <= {key}") + else: + text_object_property = o[object_key] if object_key in o else False + if text_object_property is not False: + print(f" {object_key} => {key}") + text_properties[key] = text_object_property + print(f" {dict(text_properties)=}") + print(f" {text_properties['offset']=}") + unfortunate_children = o.children + def kill_children(): + completely_delete_objects(unfortunate_children) + run_in_main_thread(kill_children) + + # found_glyphs_with_indices = [] + # for glyph_object in o.children: + # if is_glyph_object(glyph_object): + # if "glyph_index" in glyph_object: + # found_glyphs_with_indices.append(glyph_object) + # else: + # completely_delete_objects([glyph_object]) + + # found_glyphs_with_indices.sort(key=lambda g: g["glyph_index"]) + # text = "" + # for g in found_glyphs_with_indices: + # text += g["glyph_id"] + # if len(text_properties.glyphs) == len(text): + # for g in found_glyphs_with_indices: + # i = g["glyph_index"] + # gp = text_properties.glyphs[i] + # if gp["glyph_id"] == g["glyph_id"] or gp["glyph_id"] == g["glyph_id"].swapcase(): + # if "alternate" in g: + # gp["alternate"] = g["alternate"] + # for key in glyph_object_keys: + # if key in ignore_keys_in_glyph_object_comparison: + # continue + # object_key = get_key(key) + # if object_key in g: + # gp[key] = g[object_key] + # else: + # text_properties.glyphs.clear() + # # for g in found_glyphs_with_indices: + # # i = g["glyph_index"] + # # gp = text_properties.glyphs.add() + # # if gp["glyph_id"] == g["glyph_id"] or gp["glyph_id"] == g["glyph_id"].swapcase(): + # # if "alternate" in g: + # # gp["alternate"] = g["alternate"] + # # for key in glyph_object_keys: + # # if key in ignore_keys_in_glyph_object_comparison: + # # continue + # # object_key = get_key(key) + # # if object_key in g: + # # gp[key] = g[object_key] + + if "font_name" in text_properties and "face_name" in text_properties: + font_name = text_properties["font_name"] + face_name = text_properties["face_name"] + text_properties.font = f"{font_name} {face_name}" + + print("TRANSFER:: END") + + +def link_text_object_with_new_text_properties(text_object, scene=None): + if scene is None: + scene = bpy.context.scene + text_id = find_free_text_id() + text_properties = scene.abc3d_data.available_texts.add() + text_properties["text_id"] = text_id + text_object[get_key("text_id")] = text_id + print(f" found free {text_id=}") + print(" preparing text") + prepare_text(text_object[get_key("font_name")], + text_object[get_key("face_name")], + text_object[get_key("text")]) + print(" prepared text, transferring text object") + text_properties.text_object = text_object + transfer_text_object_to_text_properties(text_object, text_properties) + + +def test_finding(): + scene = bpy.context.scene + abc3d_data = scene.abc3d_data + text_id = find_free_text_id() + t = abc3d_data.available_texts.add() + print(type(t)) + t["text_id"] = text_id + print(t["text_id"]) + 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 link_text_object_and_text_properties(o, text_properties): + text_id = text_properties.text_id + 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) + +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[get_key("type")] = "glyph" + glyph_object[get_key("text_id")] = text_properties["text_id"] + +def would_regenerate(text_properties): if len(text_properties.text) != len(text_properties.glyphs): return True @@ -789,9 +1067,9 @@ def would_regenerate(text_properties): elif g.glyph_object.type != "EMPTY": return True # check if perhaps one glyph was deleted - elif type(g.glyph_object) == type(None): + elif g.glyph_object is None: return True - elif type(g.glyph_object.parent) == type(None): + elif g.glyph_object.parent is None: return True elif g.glyph_object.parent.users_collection != g.glyph_object.users_collection: return True @@ -811,7 +1089,7 @@ def update_matrices(obj): if obj.parent is None: obj.matrix_world = obj.matrix_basis - else: + # else: obj.matrix_world = obj.parent.matrix_world * \ obj.matrix_parent_inverse * \ obj.matrix_basis @@ -843,6 +1121,7 @@ def parent_to_curve(o, c): 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): """set_text_on_curve @@ -866,6 +1145,7 @@ 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 @@ -889,14 +1169,7 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, completely_delete_objects(glyph_objects, True) text_properties.glyphs.clear() - mom[f"{utils.prefix()}_type"] = "textobject" - mom[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id - mom[f"{utils.prefix()}_font_name"] = text_properties.font_name - mom[f"{utils.prefix()}_face_name"] = text_properties.face_name - mom[f"{utils.prefix()}_font_size"] = text_properties.font_size - mom[f"{utils.prefix()}_letter_spacing"] = text_properties.letter_spacing - mom[f"{utils.prefix()}_orientation"] = text_properties.orientation - mom[f"{utils.prefix()}_translation"] = text_properties.translation + transfer_text_properties_to_text_object(text_properties, mom) curve_length = get_curve_length(mom) advance = text_properties.offset @@ -982,16 +1255,11 @@ 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) - outer_node[f"{utils.prefix()}_type"] = "glyph" - outer_node[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id - outer_node[f"{utils.prefix()}_glyph_index"] = glyph_index - outer_node[f"{utils.prefix()}_font_name"] = text_properties.font_name - outer_node[f"{utils.prefix()}_face_name"] = text_properties.face_name + 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) - # bpy.context.scene.collection.objects.link(inner_node) # Parenting is hard. inner_node.parent_type = 'OBJECT' @@ -1001,6 +1269,7 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, outer_node.hide_set(True) glyph_properties["glyph_object"] = outer_node + outer_node[f"{utils.prefix()}_glyph_index"] = glyph_index else: outer_node = glyph_properties.glyph_object outer_node[f"{utils.prefix()}_glyph_index"] = glyph_index @@ -1128,52 +1397,12 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, glyph_index += 1 previous_spline_index = spline_index - # NOTE: depsgraph update not locked - # as we fixed data_path with parent_to_curve trick - # if lock_depsgraph_update_n_times < 0: - # lock_depsgraph_update_n_times = len( - # bpy.context.selected_objects - # ) - # else: - # lock_depsgraph_update_n_times += len( - # bpy.context.selected_objects - # ) - # # NOTE: we reset with a timeout, as setting and resetting certain things - # # in fast succession will cause visual glitches (e.g. {}.data.use_path). - # def reset(): - # mom.data.use_path = previous_use_path - # if counted_reset in bpy.app.handlers.depsgraph_update_post: - # bpy.app.handlers.depsgraph_update_post.remove(counted_reset) - # if bpy.app.timers.is_registered(reset): - # bpy.app.timers.unregister(reset) - # molotov = reset_depsgraph_n + 0 - # def counted_reset(scene, depsgraph): - # nonlocal molotov - # if molotov == 0: - # reset() - # else: - # molotov -= 1 - # # unregister previous resets to avoid multiple execution - # if bpy.app.timers.is_registered(reset): - # bpy.app.timers.unregister(reset) - # if counted_reset in bpy.app.handlers.depsgraph_update_post: - # bpy.app.handlers.depsgraph_update_post.remove(counted_reset) - # if not isinstance(reset_timeout_s, bool): - # if reset_timeout_s > 0: - # bpy.app.timers.register(reset, first_interval=reset_timeout_s) - # elif reset_timeout <= 0: - # reset() - # bpy.app.handlers.depsgraph_update_post.append(counted_reset) - - # endtime = time.perf_counter_ns() - # elapsedtime = endtime - starttime - return True verification_object = { f"{utils.prefix()}_type": "textobject", - f"{utils.prefix()}_linked_textobject": 0, + f"{utils.prefix()}_text_id": 0, f"{utils.prefix()}_font_name": "font_name", f"{utils.prefix()}_face_name": "face_name", f"{utils.prefix()}_font_size": 42, @@ -1187,28 +1416,6 @@ def verify_text_object(o): pass -def transfer_text_properties_to_text_object(text_properties, o): - o[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id - o[f"{utils.prefix()}_font_name"] = text_properties.font_name - o[f"{utils.prefix()}_face_name"] = text_properties.face_name - o[f"{utils.prefix()}_font_size"] = text_properties.font_size - o[f"{utils.prefix()}_letter_spacing"] = text_properties.letter_spacing - o[f"{utils.prefix()}_orientation"] = text_properties.orientation - o[f"{utils.prefix()}_translation"] = text_properties.translation - o[f"{utils.prefix()}_text"] = text_properties["text"] - - -def transfer_text_object_to_text_properties(o, text_properties): - text_properties["text_id"] = o[f"{utils.prefix()}_linked_textobject"] - text_properties["font_name"] = o[f"{utils.prefix()}_font_name"] - text_properties["face_name"] = o[f"{utils.prefix()}_face_name"] - text_properties["font_size"] = o[f"{utils.prefix()}_font_size"] - text_properties["letter_spacing"] = o[f"{utils.prefix()}_letter_spacing"] - text_properties["orientation"] = o[f"{utils.prefix()}_orientation"] - text_properties["translation"] = o[f"{utils.prefix()}_translation"] - text_properties["text"] = o[f"{utils.prefix()}_text"] - - # blender bound_box vertices # # 3------7. @@ -1686,3 +1893,4 @@ def align_origins_to_active_object(objects=None, axis=2): # c.location -= mathutils.Vector((diff, 0.0, 0.0)) @ o.matrix_world.inverted() # return "" + From 10e57dd46a93a70990c2600e2c6572152084ba8e Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sun, 25 May 2025 15:35:52 +0200 Subject: [PATCH 075/103] depsgraph detect texts implementing in depsgraph allows for duplication --- __init__.py | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/__init__.py b/__init__.py index d1695de..34bb3fb 100644 --- a/__init__.py +++ b/__init__.py @@ -1791,6 +1791,11 @@ 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): + lock_depsgraph_updates(auto_unlock_s=-1) + butils.link_text_object_with_new_text_properties(text_object, scene) + unlock_depsgraph_updates() + def detect_text(): lock_depsgraph_updates(auto_unlock_s=-1) @@ -1921,10 +1926,8 @@ def are_depsgraph_updates_locked(): import time - @persistent def on_depsgraph_update(scene, depsgraph): - global depsgraph_updates_locked print("DEPSGRAPH:: BEGIN") if not bpy.context.mode.startswith("EDIT") and not are_depsgraph_updates_locked(): for u in depsgraph.updates: @@ -1940,26 +1943,21 @@ def on_depsgraph_update(scene, depsgraph): print(f" is {len(scene.abc3d_data.available_texts)} bigger than {text_id=} is true?") # butils.detect_texts() if len(scene.abc3d_data.available_texts) > text_id: + text_properties = scene.abc3d_data.available_texts[text_id] print(" YES") - print(" is ? text object is not u.id") - if scene.abc3d_data.available_texts[text_id].text_object != u.id: - print(" yes") + print(f" is ? {text_properties.text_object.name=} is {u.id.name=}") + if text_properties.text_object == u.id.original: + print(" yes by id") else: - print(" no") + # must be duplicate + link_text_object_with_new_text_properties(u.id.original, scene) + print(" no by id") else: + # must be new thing + print(" NO, LINK TO NEW TEXTOBJECT") + link_text_object_with_new_text_properties(u.id.original, scene) print(" NO") print("DEPSGRAPH:: done textobject") - # lock_depsgraph_updates() - - # def later(): - # if butils.lock_depsgraph_update_n_times <= 0: - # butils.set_text_on_curve( - # scene.abc3d_data.available_texts[text_id] - # ) - # elif butils.lock_depsgraph_update_n_times <= 0: - # butils.lock_depsgraph_update_n_times -= 1 - - # butils.run_in_main_thread(later) if are_depsgraph_updates_locked(): print(" L O C K E D") print("DEPSGRAPH:: done") @@ -1993,6 +1991,7 @@ def register(): butils.run_in_main_thread(butils.update_available_fonts) butils.run_in_main_thread(load_used_glyphs) butils.run_in_main_thread(butils.update_types) + butils.run_in_main_thread(detect_text) Font.init() From 88cfaf3be7a09030f3e016e887ee2a2977a13efb Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sun, 25 May 2025 20:36:46 +0200 Subject: [PATCH 076/103] detect textobject and allow primitive duplication --- __init__.py | 14 +++-- butils.py | 165 ++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 151 insertions(+), 28 deletions(-) diff --git a/__init__.py b/__init__.py index 34bb3fb..b2bd911 100644 --- a/__init__.py +++ b/__init__.py @@ -137,9 +137,12 @@ 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] - ) + # butils.set_text_on_curve( + # context.scene.abc3d_data.available_texts[self.text_id] + # ) + t = butils.get_text_properties(self.text_id) + if t is not None: + butils.set_text_on_curve(t) glyph_id: bpy.props.StringProperty(maxlen=1) text_id: bpy.props.IntProperty( @@ -259,6 +262,7 @@ class ABC3D_text_properties(bpy.types.PropertyGroup): ) distribution_type: bpy.props.StringProperty() glyphs: bpy.props.CollectionProperty(type=ABC3D_glyph_properties) + actual_text: bpy.props.StringProperty() class ABC3D_data(bpy.types.PropertyGroup): @@ -1942,8 +1946,8 @@ def on_depsgraph_update(scene, depsgraph): print(" updated geometry is true") print(f" is {len(scene.abc3d_data.available_texts)} bigger than {text_id=} is true?") # butils.detect_texts() - if len(scene.abc3d_data.available_texts) > text_id: - text_properties = scene.abc3d_data.available_texts[text_id] + text_properties = butils.get_text_properties(text_id) + if text_properties is not None: print(" YES") print(f" is ? {text_properties.text_object.name=} is {u.id.name=}") if text_properties.text_object == u.id.original: diff --git a/butils.py b/butils.py index 7c38fe5..57dea5b 100644 --- a/butils.py +++ b/butils.py @@ -927,44 +927,120 @@ def get_glyph(glyph_id, font_name, face_name, notify_on_replacement=False): return glyph_tmp.original -def transfer_text_object_to_text_properties(o, text_properties, id_from_text_properties=True): +def get_text_properties(text_id, scene = None): + if scene is None: + scene = bpy.context.scene + abc3d_data = scene.abc3d_data + for t in abc3d_data.available_texts: + if text_id == t.text_id: + return t + return None + +def transfer_text_object_to_text_properties(text_object, text_properties, id_from_text_properties=True): print("TRANSFER:: BEGIN") print(f" {text_properties['text_id']=}") - print(f" {type(o)=}") + print(f" {type(text_object)=}") + 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 object_key = get_key(key) if id_from_text_properties and key == "text_id": - o[object_key] = text_properties["text_id"] + text_object[object_key] = text_properties["text_id"] print(f" {object_key} <= {key}") else: - text_object_property = o[object_key] if object_key in o else False + text_object_property = text_object[object_key] if object_key in text_object else False if text_object_property is not False: print(f" {object_key} => {key}") text_properties[key] = text_object_property print(f" {dict(text_properties)=}") print(f" {text_properties['offset']=}") - unfortunate_children = o.children - def kill_children(): - completely_delete_objects(unfortunate_children) - run_in_main_thread(kill_children) - # found_glyphs_with_indices = [] - # for glyph_object in o.children: - # if is_glyph_object(glyph_object): - # if "glyph_index" in glyph_object: - # found_glyphs_with_indices.append(glyph_object) + if len(text_object.children) == 0: + print("could be duplicate?") + if possible_brother_text_id != text_properties["text_id"] and possible_brother_text_id != "": + pass + + found_reconstructable_glyphs = False + glyph_objects_with_indices = [] + required_keys = [ + "glyph_index", + "glyph_id", + "type" + ] + for glyph_object in text_object.children: + if is_glyph_object(glyph_object): + has_required_keys = True + for key in required_keys: + if get_key(key) not in glyph_object: + has_required_keys = False + if has_required_keys: + inner_node = None + glyph_id = glyph_object[get_key("glyph_id")] + for c in glyph_object.children: + if c.name.startswith(f"{glyph_id}_mesh"): + inner_node = c + if inner_node is not None: + glyph_objects_with_indices.append(glyph_object) + + glyph_objects_with_indices.sort(key=lambda g: g[get_key("glyph_index")]) + print(f" {glyph_objects_with_indices=}") + text = "" + for g in glyph_objects_with_indices: + text += g[get_key("glyph_id")] + is_good_text = False + if len(text) > 0: + if text == text_properties.text: + is_good_text = True + print(f"{text=} is a good text because it is the same") + 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): + print(f"{text=} is a good text because it is the same considering what is possible") + is_good_text = True + if is_good_text: + print(" GOOD TEXT") + # for glyph_index, glyph_object in enumerate(glyph_objects_with_indices): + # print(f"{glyph_index}: {glyph_object}") + # if glyph_index == glyph_object[get_key("glyph_index")]: + # print("yeey glyph_index matches") # else: - # completely_delete_objects([glyph_object]) + # print("nooo glyph_idex macthes not") + # found_reconstructable_glyphs = True + text_properties.actual_text = text + text_properties.glyphs.clear() + prepare_text(text_properties.font_name, text_properties.face_name, text) + fail_after_all = False + 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) + # glyph = glyph_tmp.original + glyph_properties = text_properties.glyphs.add() + + transfer_glyph_object_to_glyph_properties(glyph_object, glyph_properties) + glyph_properties["glyph_object"] = glyph_object + glyph_properties["glyph_index"] = glyph_index + inner_node = None + for c in glyph_object.children: + if c.name.startswith(f"{glyph_id}_mesh"): + inner_node = c + print(f"found inner node {inner_node.name=} for {glyph_id=}") + if inner_node is None: + fail_after_all = True + pass + glyph_properties["glyph_object"] = glyph_object + if not fail_after_all: + found_reconstructable_glyphs = True + - # found_glyphs_with_indices.sort(key=lambda g: g["glyph_index"]) - # text = "" - # for g in found_glyphs_with_indices: - # text += g["glyph_id"] - # if len(text_properties.glyphs) == len(text): - # for g in found_glyphs_with_indices: - # i = g["glyph_index"] # gp = text_properties.glyphs[i] # if gp["glyph_id"] == g["glyph_id"] or gp["glyph_id"] == g["glyph_id"].swapcase(): # if "alternate" in g: @@ -990,6 +1066,18 @@ def transfer_text_object_to_text_properties(o, text_properties, id_from_text_pro # # if object_key in g: # # gp[key] = g[object_key] + if not found_reconstructable_glyphs: + print("KILL THE GLYPHS") + text_properties.actual_text = "" + text_properties.glyphs.clear() + unfortunate_children = text_object.children + print("KILL THE CHILDREN") + completely_delete_objects(unfortunate_children) + def kill_children(): + print("KILL THE CHILDREN") + completely_delete_objects(unfortunate_children) + run_in_main_thread(kill_children) + if "font_name" in text_properties and "face_name" in text_properties: font_name = text_properties["font_name"] face_name = text_properties["face_name"] @@ -1004,7 +1092,7 @@ def link_text_object_with_new_text_properties(text_object, scene=None): text_id = find_free_text_id() text_properties = scene.abc3d_data.available_texts.add() text_properties["text_id"] = text_id - text_object[get_key("text_id")] = text_id + # text_object[get_key("text_id")] = text_id print(f" found free {text_id=}") print(" preparing text") prepare_text(text_object[get_key("font_name")], @@ -1057,31 +1145,52 @@ def transfer_properties_to_glyph_object(text_properties, glyph_properties, glyph 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: + continue + glyph_properties[key] = glyph_object[get_key(key)] + glyph_properties["text_id"] = glyph_object[get_key("text_id")] + +import inspect def would_regenerate(text_properties): - if len(text_properties.text) != len(text_properties.glyphs): + print("REGENERATE?") + if len(text_properties.actual_text) != len(text_properties.glyphs): + print(inspect.currentframe().f_lineno) + return True + if len(text_properties.glyphs) == 0: + print(inspect.currentframe().f_lineno) return True for i, g in enumerate(text_properties.glyphs): if not hasattr(g.glyph_object, "type"): + print(inspect.currentframe().f_lineno) return True elif g.glyph_object.type != "EMPTY": + print(inspect.currentframe().f_lineno) return True # check if perhaps one glyph was deleted elif g.glyph_object is None: + print(inspect.currentframe().f_lineno) return True elif g.glyph_object.parent is None: + print(inspect.currentframe().f_lineno) return True elif g.glyph_object.parent.users_collection != g.glyph_object.users_collection: + print(inspect.currentframe().f_lineno) return True elif len(text_properties.text) > i and g.glyph_id != text_properties.text[i]: + print(inspect.currentframe().f_lineno) return True elif len(text_properties.text) > i and ( g.glyph_object[f"{utils.prefix()}_font_name"] != text_properties.font_name or g.glyph_object[f"{utils.prefix()}_face_name"] != text_properties.face_name ): + print(inspect.currentframe().f_lineno) return True + print("NOT REGENERATE") return False @@ -1123,6 +1232,9 @@ def parent_to_curve(o, c): def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, can_regenerate=False): + # for i in range (0, 42): + # print("WATCH OUT, WE DO NOT SET THE TEXT ATM") + # return False """set_text_on_curve An earlier reset cancels the other. @@ -1160,6 +1272,8 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, # mom.data.use_path = True regenerate = can_regenerate and would_regenerate(text_properties) + if regenerate: + print("RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRREGENERATE") # if we regenerate.... delete objects if regenerate and text_properties.get("glyphs"): @@ -1178,6 +1292,7 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, is_command = False previous_spline_index = -1 + actual_text = "" for i, c in enumerate(text_properties.text): face = Font.fonts[text_properties.font_name].faces[text_properties.face_name] scalor = face.unit_factor * text_properties.font_size @@ -1247,6 +1362,7 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, glyph_properties["glyph_id"] = glyph_id glyph_properties["text_id"] = text_properties.text_id glyph_properties["letter_spacing"] = 0 + actual_text += glyph_id ############### NODE SCENE MANAGEMENT @@ -1397,6 +1513,9 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, glyph_index += 1 previous_spline_index = spline_index + if regenerate: + text_properties["actual_text"] = actual_text + return True From 2ace31a246719438515b42f252833826fbed86ac Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sun, 25 May 2025 20:41:00 +0200 Subject: [PATCH 077/103] use get_text_properties instead of id as index --- __init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/__init__.py b/__init__.py index b2bd911..2d8ac95 100644 --- a/__init__.py +++ b/__init__.py @@ -1829,9 +1829,10 @@ def detect_text(): print(" {o.name=} a textobject") print(f" {type(o)=} {o.name=}") current_text_id = int(o[butils.get_key("text_id")]) + text_properties = butils.get_text_properties(current_text_id) if ( - len(abc3d_data.available_texts) > current_text_id - and abc3d_data.available_texts[current_text_id].text_object == o + text_properties is not None + and text_properties.text_object == o ): print(" {o.name=} seems fine") pass From 8f3d58aad0c068ca0f546927c8d9694f1601a4aa Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sun, 25 May 2025 22:00:54 +0200 Subject: [PATCH 078/103] transfer glyph transforms on duplication --- __init__.py | 24 +++++++++++++----------- butils.py | 44 +++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/__init__.py b/__init__.py index 2d8ac95..358b99b 100644 --- a/__init__.py +++ b/__init__.py @@ -284,17 +284,19 @@ class ABC3D_data(bpy.types.PropertyGroup): def active_text_index_update(self, context): if self.active_text_index != -1: - o = self.available_texts[self.active_text_index].text_object - # active_text_index changed. so let's update the selection - # check if it is already selected - # or perhaps one of the glyphs - if ( - not o.select_get() - and not len([c for c in o.children if c.select_get()]) > 0 - ): - bpy.ops.object.select_all(action="DESELECT") - o.select_set(True) - bpy.context.view_layer.objects.active = o + 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 + and not o.select_get() + and not len([c for c in o.children if c.select_get()]) > 0 + ): + bpy.ops.object.select_all(action="DESELECT") + o.select_set(True) + context.view_layer.objects.active = o # else: # print("already selected") diff --git a/butils.py b/butils.py index 57dea5b..b74a618 100644 --- a/butils.py +++ b/butils.py @@ -772,6 +772,15 @@ 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()) + 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, "") + return t_text def is_bezier(curve): if curve.type != "CURVE": @@ -936,6 +945,25 @@ 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): + obj_copy = obj.copy() + if add_to_collection: + if collection: + collection.objects.link(obj_copy) + elif len(obj.users_collection) > 0: + obj.users_collection[0].objects.link(obj_copy) + if data and obj.data: + obj_copy.data = obj.data.copy() + if actions and obj.animation_data: + obj_copy.animation_data.action = obj.animation_data.action.copy() + if recursive and hasattr(obj, "children"): + for child in obj.children: + child_copy = duplicate(child) + child_copy.parent_type = child.parent_type + child_copy.parent = obj_copy + # 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): print("TRANSFER:: BEGIN") print(f" {text_properties['text_id']=}") @@ -957,9 +985,18 @@ def transfer_text_object_to_text_properties(text_object, text_properties, id_fro print(f" {text_properties['offset']=}") if len(text_object.children) == 0: - print("could be duplicate?") + print("ccccccccccccccccccccccccccccccccc ccccc ccccc c c c c ccould be duplicate?") if possible_brother_text_id != text_properties["text_id"] and possible_brother_text_id != "": - pass + 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: + for child in possible_brother_object.children: + if is_glyph_object(child): + child_copy = duplicate(child) + child_copy.parent_type = child.parent_type + child_copy.parent = text_object + parent_to_curve(child_copy, text_object) + # child_copy.matrix_parent_inverse = text_object.matrix_world.inverted() found_reconstructable_glyphs = False glyph_objects_with_indices = [] @@ -1155,7 +1192,8 @@ def transfer_glyph_object_to_glyph_properties(glyph_object, glyph_properties): import inspect def would_regenerate(text_properties): print("REGENERATE?") - if len(text_properties.actual_text) != len(text_properties.glyphs): + predicted_text = predict_actual_text(text_properties) + if text_properties.actual_text != predicted_text: print(inspect.currentframe().f_lineno) return True if len(text_properties.glyphs) == 0: From e14251523bfe4f88ae5e9b76a03871de7e388022 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Mon, 26 May 2025 06:55:29 +0200 Subject: [PATCH 079/103] cleanup prints --- __init__.py | 46 +++-------------------------- butils.py | 84 ++--------------------------------------------------- 2 files changed, 6 insertions(+), 124 deletions(-) diff --git a/__init__.py b/__init__.py index 358b99b..be82bb0 100644 --- a/__init__.py +++ b/__init__.py @@ -1805,7 +1805,6 @@ def link_text_object_with_new_text_properties(text_object, scene = None): def detect_text(): lock_depsgraph_updates(auto_unlock_s=-1) - print("DETECT TEXT:: begin") scene = bpy.context.scene abc3d_data = scene.abc3d_data required_keys = [ @@ -1817,48 +1816,24 @@ def detect_text(): ] objects = scene.objects for o in objects: - print(f" {o.name=}") valid = True for key in required_keys: if butils.get_key(key) not in o: - # print(f" key {butils.get_key(key)} not there") valid = False break if not valid: continue - print(" object may be valid textobject") if o[butils.get_key("type")] == "textobject": - print(" {o.name=} a textobject") - print(f" {type(o)=} {o.name=}") 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 ): - print(" {o.name=} seems fine") + # all good pass - # t = abc3d_data.available_texts[text_id] - # a = test_availability(o[butils.get_key("font_name")], - # o[butils.get_key("face_name")], - # o[butils.get_key("text")]) - # butils.transfer_text_object_to_text_properties(o, t) else: butils.link_text_object_with_new_text_properties(o, scene) - # print(" {o.name=} is a duplicate") - # text_id = butils.find_free_text_id() - # t = abc3d_data.available_texts.add() - # t["text_id"] = text_id - # print(f" found free {text_id=}") - # print(" preparing text") - # butils.prepare_text(o[butils.get_key("font_name")], - # o[butils.get_key("face_name")], - # o[butils.get_key("text")]) - # print(" prepared text, transferring text object") - # t.text_object = o - # butils.transfer_text_object_to_text_properties(o, t) - # print(" {o.name=} transerred text object") - print("DETECT TEXT:: end") unlock_depsgraph_updates() @@ -1935,7 +1910,6 @@ import time @persistent def on_depsgraph_update(scene, depsgraph): - print("DEPSGRAPH:: BEGIN") if not bpy.context.mode.startswith("EDIT") and not are_depsgraph_updates_locked(): for u in depsgraph.updates: if ( @@ -1943,31 +1917,19 @@ def on_depsgraph_update(scene, depsgraph): and butils.get_key("type") in u.id.keys() and u.id[butils.get_key("type")] == "textobject" ): - print("DEPSGRAPH:: we have a textobject") text_id = u.id[butils.get_key("text_id")] - if u.is_updated_geometry: - print(" updated geometry is true") - print(f" is {len(scene.abc3d_data.available_texts)} bigger than {text_id=} is true?") - # butils.detect_texts() + # if u.is_updated_geometry: text_properties = butils.get_text_properties(text_id) if text_properties is not None: - print(" YES") - print(f" is ? {text_properties.text_object.name=} is {u.id.name=}") if text_properties.text_object == u.id.original: - print(" yes by id") + # nothing to do + pass else: # must be duplicate link_text_object_with_new_text_properties(u.id.original, scene) - print(" no by id") else: # must be new thing - print(" NO, LINK TO NEW TEXTOBJECT") link_text_object_with_new_text_properties(u.id.original, scene) - print(" NO") - print("DEPSGRAPH:: done textobject") - if are_depsgraph_updates_locked(): - print(" L O C K E D") - print("DEPSGRAPH:: done") def register(): diff --git a/butils.py b/butils.py index b74a618..138ed9f 100644 --- a/butils.py +++ b/butils.py @@ -871,22 +871,17 @@ def compare_text_properties_to_text_object(text_properties, o): 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: - print(f"{key}: REGENERATE {text_property=} {text_object_property}") 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]): - print(f"{key}: DIFFER {text_property=} {text_object_property}") return COMPARE_TEXT_OBJECT_DIFFER - else: - print(f"{key}: SAME {text_property.xyz=} {text_object_property.to_list()}") + # else same else: - print(f"{key}: DIFFER {text_property=} {text_object_property}") return COMPARE_TEXT_OBJECT_DIFFER - else: - print(f"{key}: SAME {text_property=} {text_object_property}") + # else same return COMPARE_TEXT_OBJECT_SAME @@ -965,9 +960,6 @@ def duplicate(obj, data=True, actions=True, add_to_collection=True, collection=N return obj_copy def transfer_text_object_to_text_properties(text_object, text_properties, id_from_text_properties=True): - print("TRANSFER:: BEGIN") - print(f" {text_properties['text_id']=}") - print(f" {type(text_object)=}") 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: @@ -975,17 +967,12 @@ def transfer_text_object_to_text_properties(text_object, text_properties, id_fro object_key = get_key(key) if id_from_text_properties and key == "text_id": text_object[object_key] = text_properties["text_id"] - print(f" {object_key} <= {key}") else: text_object_property = text_object[object_key] if object_key in text_object else False if text_object_property is not False: - print(f" {object_key} => {key}") text_properties[key] = text_object_property - print(f" {dict(text_properties)=}") - print(f" {text_properties['offset']=}") if len(text_object.children) == 0: - print("ccccccccccccccccccccccccccccccccc ccccc ccccc c c c c ccould be duplicate?") 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 @@ -1021,7 +1008,6 @@ def transfer_text_object_to_text_properties(text_object, text_properties, id_fro glyph_objects_with_indices.append(glyph_object) glyph_objects_with_indices.sort(key=lambda g: g[get_key("glyph_index")]) - print(f" {glyph_objects_with_indices=}") text = "" for g in glyph_objects_with_indices: text += g[get_key("glyph_id")] @@ -1029,7 +1015,6 @@ def transfer_text_object_to_text_properties(text_object, text_properties, id_fro if len(text) > 0: if text == text_properties.text: is_good_text = True - print(f"{text=} is a good text because it is the same") 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()) @@ -1039,17 +1024,8 @@ def transfer_text_object_to_text_properties(text_object, text_properties, id_fro for c in AVAILABILITY["missing"]: t_text = t_text.replace(c, "") if len(t_text) == len(text): - print(f"{text=} is a good text because it is the same considering what is possible") is_good_text = True if is_good_text: - print(" GOOD TEXT") - # for glyph_index, glyph_object in enumerate(glyph_objects_with_indices): - # print(f"{glyph_index}: {glyph_object}") - # if glyph_index == glyph_object[get_key("glyph_index")]: - # print("yeey glyph_index matches") - # else: - # print("nooo glyph_idex macthes not") - # found_reconstructable_glyphs = True text_properties.actual_text = text text_properties.glyphs.clear() prepare_text(text_properties.font_name, text_properties.face_name, text) @@ -1069,7 +1045,6 @@ def transfer_text_object_to_text_properties(text_object, text_properties, id_fro for c in glyph_object.children: if c.name.startswith(f"{glyph_id}_mesh"): inner_node = c - print(f"found inner node {inner_node.name=} for {glyph_id=}") if inner_node is None: fail_after_all = True pass @@ -1077,41 +1052,12 @@ def transfer_text_object_to_text_properties(text_object, text_properties, id_fro if not fail_after_all: found_reconstructable_glyphs = True - - # gp = text_properties.glyphs[i] - # if gp["glyph_id"] == g["glyph_id"] or gp["glyph_id"] == g["glyph_id"].swapcase(): - # if "alternate" in g: - # gp["alternate"] = g["alternate"] - # for key in glyph_object_keys: - # if key in ignore_keys_in_glyph_object_comparison: - # continue - # object_key = get_key(key) - # if object_key in g: - # gp[key] = g[object_key] - # else: - # text_properties.glyphs.clear() - # # for g in found_glyphs_with_indices: - # # i = g["glyph_index"] - # # gp = text_properties.glyphs.add() - # # if gp["glyph_id"] == g["glyph_id"] or gp["glyph_id"] == g["glyph_id"].swapcase(): - # # if "alternate" in g: - # # gp["alternate"] = g["alternate"] - # # for key in glyph_object_keys: - # # if key in ignore_keys_in_glyph_object_comparison: - # # continue - # # object_key = get_key(key) - # # if object_key in g: - # # gp[key] = g[object_key] - if not found_reconstructable_glyphs: - print("KILL THE GLYPHS") text_properties.actual_text = "" text_properties.glyphs.clear() unfortunate_children = text_object.children - print("KILL THE CHILDREN") completely_delete_objects(unfortunate_children) def kill_children(): - print("KILL THE CHILDREN") completely_delete_objects(unfortunate_children) run_in_main_thread(kill_children) @@ -1120,8 +1066,6 @@ def transfer_text_object_to_text_properties(text_object, text_properties, id_fro face_name = text_properties["face_name"] text_properties.font = f"{font_name} {face_name}" - print("TRANSFER:: END") - def link_text_object_with_new_text_properties(text_object, scene=None): if scene is None: @@ -1130,12 +1074,9 @@ 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 - print(f" found free {text_id=}") - print(" preparing text") prepare_text(text_object[get_key("font_name")], text_object[get_key("face_name")], text_object[get_key("text")]) - print(" prepared text, transferring text object") text_properties.text_object = text_object transfer_text_object_to_text_properties(text_object, text_properties) @@ -1145,9 +1086,7 @@ def test_finding(): abc3d_data = scene.abc3d_data text_id = find_free_text_id() t = abc3d_data.available_texts.add() - print(type(t)) t["text_id"] = text_id - print(t["text_id"]) o = bpy.context.active_object transfer_text_object_to_text_properties(o, t) @@ -1189,46 +1128,34 @@ 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")] -import inspect def would_regenerate(text_properties): - print("REGENERATE?") predicted_text = predict_actual_text(text_properties) if text_properties.actual_text != predicted_text: - print(inspect.currentframe().f_lineno) return True if len(text_properties.glyphs) == 0: - print(inspect.currentframe().f_lineno) return True for i, g in enumerate(text_properties.glyphs): if not hasattr(g.glyph_object, "type"): - print(inspect.currentframe().f_lineno) return True elif g.glyph_object.type != "EMPTY": - print(inspect.currentframe().f_lineno) return True # check if perhaps one glyph was deleted elif g.glyph_object is None: - print(inspect.currentframe().f_lineno) return True elif g.glyph_object.parent is None: - print(inspect.currentframe().f_lineno) return True elif g.glyph_object.parent.users_collection != g.glyph_object.users_collection: - print(inspect.currentframe().f_lineno) return True elif len(text_properties.text) > i and g.glyph_id != text_properties.text[i]: - print(inspect.currentframe().f_lineno) return True elif len(text_properties.text) > i and ( g.glyph_object[f"{utils.prefix()}_font_name"] != text_properties.font_name or g.glyph_object[f"{utils.prefix()}_face_name"] != text_properties.face_name ): - print(inspect.currentframe().f_lineno) return True - print("NOT REGENERATE") return False @@ -1270,9 +1197,6 @@ def parent_to_curve(o, c): def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, can_regenerate=False): - # for i in range (0, 42): - # print("WATCH OUT, WE DO NOT SET THE TEXT ATM") - # return False """set_text_on_curve An earlier reset cancels the other. @@ -1310,13 +1234,9 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, # mom.data.use_path = True regenerate = can_regenerate and would_regenerate(text_properties) - if regenerate: - print("RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRREGENERATE") # if we regenerate.... delete objects if regenerate and text_properties.get("glyphs"): - for g in text_properties.glyphs: - print(dict(g)) glyph_objects = [g["glyph_object"] for g in text_properties["glyphs"]] completely_delete_objects(glyph_objects, True) text_properties.glyphs.clear() From 777644e509c6f4eb953840de222e771ca93edc0c Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Mon, 26 May 2025 06:57:13 +0200 Subject: [PATCH 080/103] bump version to v0.0.9 --- README.md | 2 +- __init__.py | 2 +- common/utils.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c3c8ddc..bb598e9 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ / ___ \| |_) | |___ ___) | |_| | /_/ \_\____/ \____|____/|____/ ``` -v0.0.8 +v0.0.9 Convenience tool to work with 3D typography in Blender and Cinema4D. diff --git a/__init__.py b/__init__.py index be82bb0..3d7f6bc 100644 --- a/__init__.py +++ b/__init__.py @@ -16,7 +16,7 @@ from .common import Font, utils bl_info = { "name": "ABC3D", "author": "Jakob Schlötter, Studio Pointer*", - "version": (0, 0, 8), + "version": (0, 0, 9), "blender": (4, 1, 0), "location": "VIEW3D", "description": "Convenience addon for 3D fonts", diff --git a/common/utils.py b/common/utils.py index 0605d60..796d3e6 100644 --- a/common/utils.py +++ b/common/utils.py @@ -8,7 +8,7 @@ def get_version_minor(): def get_version_patch(): - return 8 + return 9 def get_version_string(): From 335ab1facea387ca6fab652a6bff22ec8df620a5 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Thu, 29 May 2025 15:27:24 +0200 Subject: [PATCH 081/103] 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): From 513497d49239db8ece979f58e694fabce7636ce4 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Thu, 29 May 2025 15:39:26 +0200 Subject: [PATCH 082/103] better notices --- __init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__init__.py b/__init__.py index 69331d8..b5bd7ac 100644 --- a/__init__.py +++ b/__init__.py @@ -1614,7 +1614,7 @@ class ABC3D_OT_SaveFontToFile(bpy.types.Operator): return None bpy.app.timers.register(lambda: remove_faces(), first_interval=2) - self.report({"INFO"}, "did it") + self.report({"INFO"}, f"{utils.prefix()}::save_font_to_file done") return {"FINISHED"} From 8965ab11eb449d4571a682ccf181fd12a7cbd42e Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Thu, 29 May 2025 19:28:35 +0200 Subject: [PATCH 083/103] [feature] print line number --- common/utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/common/utils.py b/common/utils.py index d7f3fab..40a114c 100644 --- a/common/utils.py +++ b/common/utils.py @@ -82,6 +82,10 @@ def open_file_browser(directory): # xdg-open *should* be supported by recent Gnome, KDE, Xfce +def LINE(): + return sys._getframe(1).f_lineno + + def printerr(*args, **kwargs): print(*args, file=sys.stderr, **kwargs) From 3ef2ae934da7ebc89809f5801e0c20b0b8a4b0a5 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Thu, 29 May 2025 19:29:34 +0200 Subject: [PATCH 084/103] [optimization] skip bezier skip bezier if all handles sit on their points --- butils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/butils.py b/butils.py index a8a9a14..3fe2098 100644 --- a/butils.py +++ b/butils.py @@ -94,6 +94,8 @@ def calc_point_on_bezier(bezier_point_1, bezier_point_2, t): h1 = bezier_point_1.handle_right p2 = bezier_point_2.co h2 = bezier_point_2.handle_left + if p1 == h1 and p2 == h2: + return p1 + t * (p2 - p1) return ( ((1 - t) ** 3) * p1 + (3 * t * (1 - t) ** 2) * h1 @@ -126,6 +128,8 @@ def calc_tangent_on_bezier(bezier_point_1, bezier_point_2, t): h1 = bezier_point_1.handle_right p2 = bezier_point_2.co h2 = bezier_point_2.handle_left + if p1 == h1 and p2 == h2: + return (p2 - p1).normalized() return ( (-3 * (1 - t) ** 2) * p1 + (-6 * t * (1 - t) + 3 * (1 - t) ** 2) * h1 From 963d89daf9804138744004e322a80913322d63df Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Thu, 29 May 2025 19:29:56 +0200 Subject: [PATCH 085/103] useful comment --- butils.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/butils.py b/butils.py index 3fe2098..11a30ca 100644 --- a/butils.py +++ b/butils.py @@ -138,6 +138,24 @@ def calc_tangent_on_bezier(bezier_point_1, bezier_point_2, t): ).normalized() +# 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 + + +# a = TestCalcPoint(mathutils.Vector((0,0,0)), handle_right=mathutils.Vector((0,1,0))) +# b = TestCalcPoint(mathutils.Vector((1,0,0)), handle_left=mathutils.Vector((1,1,0))) +# c = TestCalcPoint(mathutils.Vector((0,0,0)), handle_right=mathutils.Vector((0,0,0))) +# d = TestCalcPoint(mathutils.Vector((1,0,0)), handle_left=mathutils.Vector((1,0,0))) +# calc_point_on_bezier(a,b,0.5) + def align_rotations_auto_pivot( From 04229fbc311ff74001e38b86de72160a58c00582 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Thu, 29 May 2025 19:32:56 +0200 Subject: [PATCH 086/103] [fix] fix bezier when individual handles sit on points bonus: prevent eternal while loop --- butils.py | 97 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 68 insertions(+), 29 deletions(-) diff --git a/butils.py b/butils.py index 11a30ca..557054a 100644 --- a/butils.py +++ b/butils.py @@ -215,6 +215,34 @@ def calc_bezier_length(bezier_point_1, bezier_point_2, resolution=20): return length +def get_real_beziers_and_lengths(bezier_spline_obj, resolution_factor): + beziers = [] + lengths = [] + total_length = 0 + n_bezier_points = len(bezier_spline_obj.bezier_points) + real_n_bezier_points = len(bezier_spline_obj.bezier_points) + if bezier_spline_obj.use_cyclic_u: + n_bezier_points += 1 + for i in range(0, n_bezier_points - 1): + i_a = i % (n_bezier_points - 1) + i_b = (i_a + 1) % real_n_bezier_points + bezier = [ + bezier_spline_obj.bezier_points[i_a], + bezier_spline_obj.bezier_points[i_b], + ] + length = calc_bezier_length( + bezier[0], + bezier[1], + int(bezier_spline_obj.resolution_u * resolution_factor), + ) + total_length += length + beziers.append(bezier) + lengths.append(length) + # if total_length > distance: + # break + return beziers, lengths, total_length + + def calc_point_on_bezier_spline( bezier_spline_obj, distance, output_tangent=False, resolution_factor=1.0 ): @@ -241,6 +269,17 @@ def calc_point_on_bezier_spline( if distance <= 0: p = bezier_spline_obj.bezier_points[0] travel = (p.co - p.handle_left).normalized() * distance + + # in case the handles sit on the points + # we interpolate the travel from points of the bezier + # 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) + travel_point = calc_point_on_bezier(beziers[0][1], beziers[0][0], 0.001) + travel = travel_point.normalized() * distance + location = p.co + travel if output_tangent: p2 = bezier_spline_obj.bezier_points[1] @@ -249,30 +288,8 @@ def calc_point_on_bezier_spline( else: return location - beziers = [] - lengths = [] - total_length = 0 - n_bezier_points = len(bezier_spline_obj.bezier_points) - real_n_bezier_points = len(bezier_spline_obj.bezier_points) - if bezier_spline_obj.use_cyclic_u: - n_bezier_points += 1 - for i in range(0, n_bezier_points - 1): - i_a = i % (n_bezier_points - 1) - i_b = (i_a + 1) % real_n_bezier_points - bezier = [ - bezier_spline_obj.bezier_points[i_a], - bezier_spline_obj.bezier_points[i_b], - ] - length = calc_bezier_length( - bezier[0], - bezier[1], - int(bezier_spline_obj.resolution_u * resolution_factor), - ) - total_length += length - beziers.append(bezier) - lengths.append(length) - # if total_length > distance: - # break + beziers, lengths, total_length = get_real_beziers_and_lengths(bezier_spline_obj, + resolution_factor) iterated_distance = 0 for i in range(0, len(beziers)): @@ -290,7 +307,16 @@ 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 + # we interpolate the travel from points of the bezier + # 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_right == p.co and len(beziers) > 0: + travel_point = calc_point_on_bezier(beziers[-1][1], beziers[-1][0], 0.001) + travel = travel_point.normalized() * (distance - total_length) location = p.co + travel if output_tangent: tangent = calc_tangent_on_bezier(beziers[last_i][0], p, 1) @@ -580,7 +606,7 @@ def update_available_fonts(): f = abc3d_data.available_fonts.add() f.font_name = font_name f.face_name = face_name - print(f"{__name__} added {font_name} {face_name}") + print(f"{utils.prefix()}::update_available_fonts: {__name__} added {font_name} {face_name}") # def update_available_texts(): @@ -1474,26 +1500,39 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, mom, advance + glyph_advance, False, True ) if psi == si: + n_max = 100 + n = 0 while ( previous_location - new_location - ).length > glyph_advance and psi == si: + ).length > glyph_advance and psi == si and n < n_max: curve_compensation = curve_compensation - glyph_advance * 0.01 - new_location, si = calc_point_on_bezier_curve( + tmp_new_location, si = calc_point_on_bezier_curve( mom, advance + glyph_advance + curve_compensation, output_tangent=False, output_spline_index=True, ) + if tmp_new_location == new_location: + 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: + ).length < glyph_advance and psi == si and n < n_max: curve_compensation = curve_compensation + glyph_advance * 0.01 - new_location, si = calc_point_on_bezier_curve( + tmp_new_location, si = calc_point_on_bezier_curve( mom, advance + glyph_advance + curve_compensation, output_tangent=False, output_spline_index=True, ) + if tmp_new_location == new_location: + print(f"{utils.prefix()}::set_text_on_curve::compensate_curvature while loop overstaying welcome") + break + new_location = tmp_new_location + n += 1 advance = advance + glyph_advance + curve_compensation glyph_index += 1 From 19f4bf586fb374c41680a3aa577fba9b1f0eb0b9 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Thu, 29 May 2025 19:34:15 +0200 Subject: [PATCH 087/103] bump version to v0.0.10 --- README.md | 2 +- __init__.py | 2 +- common/utils.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index bb598e9..7316d62 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ / ___ \| |_) | |___ ___) | |_| | /_/ \_\____/ \____|____/|____/ ``` -v0.0.9 +v0.0.10 Convenience tool to work with 3D typography in Blender and Cinema4D. diff --git a/__init__.py b/__init__.py index b5bd7ac..55ef467 100644 --- a/__init__.py +++ b/__init__.py @@ -16,7 +16,7 @@ from .common import Font, utils bl_info = { "name": "ABC3D", "author": "Jakob Schlötter, Studio Pointer*", - "version": (0, 0, 9), + "version": (0, 0, 10), "blender": (4, 1, 0), "location": "VIEW3D", "description": "Convenience addon for 3D fonts", diff --git a/common/utils.py b/common/utils.py index 40a114c..3ad1e34 100644 --- a/common/utils.py +++ b/common/utils.py @@ -8,7 +8,7 @@ def get_version_minor(): def get_version_patch(): - return 9 + return 10 def get_version_string(): From 94236591536c6eb01b6f870f65cfffe1c5c857ef Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sat, 31 May 2025 16:12:40 +0200 Subject: [PATCH 088/103] update requirements.txt --- requirements.txt | 58 ++++++++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/requirements.txt b/requirements.txt index fcb9ea9..ada7e47 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,33 +1,39 @@ -astroid==3.3.5 -attrs==24.2.0 -black==24.10.0 -bpy==4.2.0 -cattrs==24.1.2 -certifi==2024.8.30 -charset-normalizer==3.4.0 -click==8.1.7 -Cython==3.0.11 -dill==0.3.9 -docstring-to-markdown==0.15 -flake8==7.1.1 +asttokens==3.0.0 +attrs==25.3.0 +bpy==4.4.0 +cattrs==24.1.3 +certifi==2025.4.26 +charset-normalizer==3.4.2 +Cython==3.1.1 +decorator==5.2.1 +docstring-to-markdown==0.17 +executing==2.2.0 idna==3.10 -isort==5.13.2 -jedi==0.19.1 -jedi-language-server==0.41.4 +importlib_metadata==8.7.0 +ipython==9.2.0 +ipython_pygments_lexers==1.1.1 +jedi==0.19.2 +jedi-language-server==0.45.1 lsprotocol==2023.0.1 mathutils==3.3.0 -mccabe==0.7.0 -mypy-extensions==1.0.0 -numpy==2.1.3 -packaging==24.1 +matplotlib-inline==0.1.7 +numpy==1.26.4 parso==0.8.4 -pathspec==0.12.1 -platformdirs==4.3.6 -pycodestyle==2.12.1 -pyflakes==3.2.0 +pexpect==4.9.0 +pluggy==1.6.0 +prompt_toolkit==3.0.51 +ptyprocess==0.7.0 +pure_eval==0.2.3 pygls==1.3.1 -pylint==3.3.1 +Pygments==2.19.1 +python-jsonrpc-server==0.4.0 +python-lsp-jsonrpc==1.1.2 requests==2.32.3 -tomlkit==0.13.2 -urllib3==2.2.3 +stack-data==0.6.3 +traitlets==5.14.3 +typing_extensions==4.13.2 +ujson==5.10.0 +urllib3==2.4.0 +wcwidth==0.2.13 +zipp==3.22.0 zstandard==0.23.0 From 2dcd4e7a2c61de015889d8e3291752fc115d193d Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sat, 31 May 2025 16:13:16 +0200 Subject: [PATCH 089/103] [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 From 7a43cfaf2f2ef43b9a440c9f8c67974685e0cd1f Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sat, 31 May 2025 16:31:50 +0200 Subject: [PATCH 090/103] [feature] unload glyphs and refresh fonts --- __init__.py | 85 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 57 insertions(+), 28 deletions(-) diff --git a/__init__.py b/__init__.py index 6e071a0..19043fb 100644 --- a/__init__.py +++ b/__init__.py @@ -383,7 +383,10 @@ class ABC3D_PT_FontList(bpy.types.Panel): abc3d_data, "active_font_index", ) - if abc3d_data.active_font_index >= 0: + if ( + abc3d_data.active_font_index >= 0 + and len(abc3d_data.available_fonts) > abc3d_data.active_font_index + ): available_font = abc3d_data.available_fonts[abc3d_data.active_font_index] font_name = available_font.font_name face_name = available_font.face_name @@ -432,6 +435,15 @@ class ABC3D_PT_FontList(bpy.types.Panel): ) oper_lf.font_name = font_name oper_lf.face_name = face_name + box = layout.box() + row = box.row() + row.label(text="File and Memory optimization") + row = box.row() + row.operator(f"{__name__}.refresh_fonts", text="Refresh Font list from disk") + row = box.row() + row.operator( + f"{__name__}.unload_unused_glyphs", text="Unload unused glyphs from memory" + ) class ABC3D_PT_TextPlacement(bpy.types.Panel): @@ -773,33 +785,6 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): return g return None - # def font_items_callback(self, context): - # items = [] - # fonts = Font.get_loaded_fonts_and_faces() - # for f in fonts: - # items.append((f"{f[0]} {f[1]}", f"{f[0]} {f[1]}", "")) - # return items - - # def font_default_callback(self, context): - # t = self.get_active_text_properties(self) - # if type(t) != type(None): - # return f"{t.font_name} {t.face_name}" - # else: - # return None - - # def font_update_callback(self, context): - # font_name, face_name = self.font.split(" ") - # t = self.get_active_text_properties(self) - # t.font_name = font_name - # t.face_name = face_name - # butils.set_text_on_curve(t) - - # font: bpy.props.EnumProperty( - # items=font_items_callback, - # default=font_default_callback, - # update=font_update_callback, - # ) - @classmethod def poll(self, context): try: @@ -843,6 +828,33 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): box.row().prop(glyph_props, "letter_spacing") +class ABC3D_OT_RefreshAvailableFonts(bpy.types.Operator): + """Refreshes available font list from disk. + This also removes all fonts which are not saved in the asset directory. + Can be useful when creating fonts or manually installing fonts.""" + + bl_idname = f"{__name__}.refresh_fonts" + bl_label = "Refresh Available Fonts" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + refresh_fonts() + return {"FINISHED"} + + +class ABC3D_OT_UnloadUnusedGlyphs(bpy.types.Operator): + """Unload all glyphs which are not actively used in this project from memory. + They will still be normally loaded when you use them again.""" + + bl_idname = f"{__name__}.unload_unused_glyphs" + bl_label = "Unload Unused Glyphs" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + butils.unload_unused_glyphs() + return {"FINISHED"} + + class ABC3D_OT_InstallFont(bpy.types.Operator): """Install or load Fontfile from path above. (Format must be *.glb or *.gltf)""" @@ -1851,6 +1863,8 @@ classes = ( ABC3D_PT_NamingHelper, ABC3D_PT_TextPropertiesPanel, ABC3D_OT_OpenAssetDirectory, + ABC3D_OT_RefreshAvailableFonts, + ABC3D_OT_UnloadUnusedGlyphs, ABC3D_OT_LoadInstalledFonts, ABC3D_OT_LoadFont, ABC3D_OT_AddDefaultMetrics, @@ -1954,6 +1968,21 @@ def load_used_glyphs(): butils.load_font_from_filepath(fp, a.unloaded) +def refresh_fonts(): + fontcollection: bpy_types.Collection = bpy.data.collections.get("ABC3D") + if fontcollection is not None: + objs = [o for o in fontcollection.objects if o.parent == None] + butils.completely_delete_objects(objs) + 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) + butils.run_in_main_thread(load_used_glyphs) + butils.run_in_main_thread(butils.update_types) + butils.run_in_main_thread(detect_text) + butils.run_in_main_thread(butils.unload_unused_glyphs) + + @persistent def load_handler(self, dummy): if not bpy.app.timers.is_registered(butils.execute_queued_functions): From 01fcb60e31d9cb5c1fd4ef776902da8a80d6fdd8 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sat, 31 May 2025 17:11:07 +0200 Subject: [PATCH 091/103] fix version string --- common/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/utils.py b/common/utils.py index 3ad1e34..77e4e8b 100644 --- a/common/utils.py +++ b/common/utils.py @@ -12,7 +12,7 @@ def get_version_patch(): def get_version_string(): - return f"{get_version_major()}.{get_version_minor()}.{get_version_patch}" + return f"{get_version_major()}.{get_version_minor()}.{get_version_patch()}" def prefix(): From 8470425d20eed1f3da747e6fe7102fb9c5285c6f Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sat, 31 May 2025 17:16:59 +0200 Subject: [PATCH 092/103] potential fix i cannot imagine how the while loop would go on forever, but hard limits are nice --- common/utils.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/common/utils.py b/common/utils.py index 77e4e8b..743b7cd 100644 --- a/common/utils.py +++ b/common/utils.py @@ -23,7 +23,6 @@ import datetime import time - def get_timestamp(): return datetime.datetime.fromtimestamp(time.time()).strftime("%Y.%m.%d-%H:%M:%S") @@ -93,13 +92,17 @@ 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): +import os +import pathlib + + +def can_create_path(path_str: str): path = pathlib.Path(path_str).absolute().resolve() - while True: # this looks dangerours, but it actually is not + tries = 0 + maximum_tries = 1000 + while True: if path.exists(): if os.access(path, os.W_OK): return True @@ -111,7 +114,11 @@ def can_create_path(path_str : str): return False path = path.parent - + tries += 1 + if tries > maximum_tries: + # always, always break out of while loops eventually + # IF you don't want to be here forever + break # # Evaluate a bezier curve for the parameter 0<=t<=1 along its length From d61607c75d96dcf0a2aa9eaa401620f8f349ba91 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sat, 31 May 2025 17:47:47 +0200 Subject: [PATCH 093/103] [fix] deletion fixes+ and some smaller cosmetic changes --- __init__.py | 26 +++++++++++++++++--------- butils.py | 30 ++++++++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/__init__.py b/__init__.py index 19043fb..3dbc369 100644 --- a/__init__.py +++ b/__init__.py @@ -336,7 +336,7 @@ class ABC3D_UL_texts(bpy.types.UIList): class ABC3D_PT_Panel(bpy.types.Panel): - bl_label = f"{__name__} panel" + bl_label = f"{utils.prefix()} Panel" bl_category = "ABC3D" bl_space_type = "VIEW_3D" bl_region_type = "UI" @@ -344,6 +344,9 @@ class ABC3D_PT_Panel(bpy.types.Panel): def draw(self, context): layout = self.layout + row = layout.row() + row.label(text=f"{utils.prefix()} v{utils.get_version_string()}") + icon = "NONE" if len(context.scene.abc3d_data.available_fonts) == 0: icon = "ERROR" @@ -439,11 +442,9 @@ class ABC3D_PT_FontList(bpy.types.Panel): row = box.row() row.label(text="File and Memory optimization") row = box.row() - row.operator(f"{__name__}.refresh_fonts", text="Refresh Font list from disk") + row.operator(f"{__name__}.refresh_fonts", text="Refresh font list from disk") row = box.row() - row.operator( - f"{__name__}.unload_unused_glyphs", text="Unload unused glyphs from memory" - ) + row.operator(f"{__name__}.unload_unused_glyphs", text="Unload unused glyphs") class ABC3D_PT_TextPlacement(bpy.types.Panel): @@ -1013,7 +1014,6 @@ class ABC3D_OT_LoadInstalledFonts(bpy.types.Operator): return context.window_manager.invoke_props_dialog(self) def execute(self, context): - print("EXECUTE LOAD INSTALLED FONTS") scene = bpy.context.scene if self.load_into_memory: @@ -1234,6 +1234,7 @@ class ABC3D_OT_RemoveText(bpy.types.Operator): def execute(self, context): abc3d_data = context.scene.abc3d_data + lock_depsgraph_updates(auto_unlock_s=-1) if abc3d_data.active_text_index < 0: butils.ShowMessageBox( title="No text selected", @@ -1269,6 +1270,7 @@ class ABC3D_OT_RemoveText(bpy.types.Operator): butils.simply_delete_objects(remove_list) abc3d_data.available_texts.remove(i) + unlock_depsgraph_updates() return {"FINISHED"} @@ -2033,6 +2035,7 @@ import time @persistent def on_depsgraph_update(scene, depsgraph): if not bpy.context.mode.startswith("EDIT") and not are_depsgraph_updates_locked(): + lock_depsgraph_updates(auto_unlock_s=-1) for u in depsgraph.updates: if ( butils.get_key("text_id") in u.id.keys() @@ -2046,12 +2049,17 @@ def on_depsgraph_update(scene, depsgraph): if text_properties.text_object == u.id.original: # nothing to do pass - else: + elif butils.is_text_object_legit(u.id.original): # must be duplicate link_text_object_with_new_text_properties(u.id.original, scene) - else: - # must be new thing + elif ( + butils.is_text_object_legit(u.id.original) + and len(u.id.original.users_collection) > 0 + ): + # must be a new thing, maybe manually created or so link_text_object_with_new_text_properties(u.id.original, scene) + butils.clean_text_properties() + unlock_depsgraph_updates() def register(): diff --git a/butils.py b/butils.py index 2ca1c81..c24c891 100644 --- a/butils.py +++ b/butils.py @@ -647,6 +647,17 @@ def is_glyph_used(glyph_alternates): return False +def clean_text_properties(): + abc3d_data = bpy.context.scene.abc3d_data + remove_these = [] + for i, text_properties in enumerate(abc3d_data.available_texts): + if len(text_properties.text_object.users_collection) <= 0: + remove_these.append(i) + remove_these.reverse() + for i in remove_these: + abc3d_data.available_texts.remove(i) + + def clean_fontcollection(fontcollection=None): if fontcollection is None: fontcollection = bpy.data.collections.get("ABC3D") @@ -1336,6 +1347,21 @@ def test_finding(): transfer_text_object_to_text_properties(o, t) +def is_text_object_legit(text_object): + must_have_keys = [ + get_key("font_name"), + get_key("face_name"), + get_key("text"), + get_key("type"), + ] + for key in must_have_keys: + if key not in text_object: + return False + if text_object[get_key("type")] != "textobject": + return False + return True + + # def detect_texts(): # scene = bpy.context.scene # abc3d_data = scene.abc3d_data @@ -1478,6 +1504,8 @@ def set_text_on_curve( mom = text_properties.text_object if mom.type != "CURVE": return False + if len(mom.users_collection) < 1: + return False distribution_type = "CALCULATE" if is_bezier(mom) else "FOLLOW_PATH" @@ -2042,8 +2070,6 @@ 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 = [] From 6160b99c93c2ce390785c978ea6b2075b1f9154d Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sat, 31 May 2025 19:53:37 +0200 Subject: [PATCH 094/103] [fix] recursive selections --- __init__.py | 146 ++++++++++++++-------------------------------------- butils.py | 8 +++ 2 files changed, 46 insertions(+), 108 deletions(-) diff --git a/__init__.py b/__init__.py index 3dbc369..c27a677 100644 --- a/__init__.py +++ b/__init__.py @@ -269,8 +269,9 @@ class ABC3D_data(bpy.types.PropertyGroup): ) def active_text_index_update(self, context): + lock_depsgraph_updates() if self.active_text_index != -1: - text_properties = butils.get_text_properties( + text_properties = butils.get_text_properties_by_index( self.active_text_index, context.scene ) if text_properties is not None: @@ -278,16 +279,20 @@ class ABC3D_data(bpy.types.PropertyGroup): # 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 - and not o.select_get() - and not len([c for c in o.children if c.select_get()]) > 0 + if os is not None and not butils.is_or_has_parent( + context.active_object, o ): + # if ( + # o is not None + # and not o.select_get() + # and not len([c for c in o.children if c.select_get()]) > 0 + # ): bpy.ops.object.select_all(action="DESELECT") o.select_set(True) context.view_layer.objects.active = o - # else: - # print("already selected") + unlock_depsgraph_updates() + # else: + # print("already selected") active_text_index: bpy.props.IntProperty(update=active_text_index_update) @@ -487,99 +492,6 @@ class ABC3D_PT_TextManagement(bpy.types.Panel): bl_region_type = "UI" bl_options = {"DEFAULT_CLOSED"} - # TODO: perhaps this should be done in a periodic timer - @classmethod - def poll(self, context): - scene = context.scene - abc3d_data = scene.abc3d_data - # TODO: update available_texts - - def update(): - if bpy.context.screen.is_animation_playing: - return - active_text_index = -1 - remove_list = [] - for i, t in enumerate(abc3d_data.available_texts): - if type(t.text_object) == type(None): - remove_list.append(i) - continue - remove_me = True - for c in t.text_object.children: - if ( - len(c.users_collection) > 0 - and not isinstance(c.get(f"{utils.prefix()}_text_id"), None) - and c.get(f"{utils.prefix()}_text_id") == t.text_id - ): - remove_me = False - # not sure how to solve this reliably atm, - # we need to reassign the glyph, but also get the proper properties from glyph_properties - # these might be there in t.glyphs, but linked to removed objects - # or they might be lost - if type( - next( - ( - g - for g in t.glyphs - if type(g.glyph_object) == type(None) - ), - None, - ) - ) == type(None): - g = next( - ( - g - for g in t.glyphs - if type(g.glyph_object) == type(None) - ), - None, - ) - # for g in t.glyphs: - # if type(g) == type(None): - # print("IS NONE") - # if type(g.glyph_object) == type(None): - # print("go IS NONE") - # else: - # if g.glyph_object == c: - # # print(g.glyph_object.name) - # pass - - if remove_me: - remove_list.append(i) - - for i in remove_list: - if abc3d_data.available_texts[i].text_object is not None: - mom = abc3d_data.available_texts[i].text_object - - def delif(o, p): - if p in o: - del o[p] - - delif(mom, f"{utils.prefix()}_text_id") - delif(mom, f"{utils.prefix()}_font_name") - delif(mom, f"{utils.prefix()}_face_name") - delif(mom, f"{utils.prefix()}_font_size") - delif(mom, f"{utils.prefix()}_letter_spacing") - delif(mom, f"{utils.prefix()}_orientation") - delif(mom, f"{utils.prefix()}_translation") - delif(mom, f"{utils.prefix()}_offset") - abc3d_data.available_texts.remove(i) - - for i, t in enumerate(abc3d_data.available_texts): - if context.active_object == t.text_object: - active_text_index = i - if ( - hasattr(context.active_object, "parent") - and context.active_object.parent == t.text_object - ): - active_text_index = i - - if active_text_index != abc3d_data.active_text_index: - abc3d_data.active_text_index = active_text_index - - # butils.run_in_main_thread(update) - - return True - def draw(self, context): layout = self.layout wm = context.window_manager @@ -1234,7 +1146,7 @@ class ABC3D_OT_RemoveText(bpy.types.Operator): def execute(self, context): abc3d_data = context.scene.abc3d_data - lock_depsgraph_updates(auto_unlock_s=-1) + lock_depsgraph_updates() if abc3d_data.active_text_index < 0: butils.ShowMessageBox( title="No text selected", @@ -1354,10 +1266,7 @@ class ABC3D_OT_PlaceText(bpy.types.Operator): distribution_type = "DEFAULT" - text_id = 0 - for i, tt in enumerate(abc3d_data.available_texts): - while text_id == tt.text_id: - text_id = text_id + 1 + text_id = butils.find_free_text_id() t = abc3d_data.available_texts.add() # If you wish to set a value and not fire an update, set the id property. # A property defined via bpy.props for example ob.prop is stored as ob["prop"] once set to non default. @@ -1907,13 +1816,33 @@ def compare_text_object_with_object(t, o, strict=False): def link_text_object_with_new_text_properties(text_object, scene=None): - lock_depsgraph_updates(auto_unlock_s=-1) + lock_depsgraph_updates() butils.link_text_object_with_new_text_properties(text_object, scene) unlock_depsgraph_updates() +def determine_active_text_index_from_selection(): + if bpy.context.active_object is None: + return -1 + for text_index, text_properties in enumerate( + bpy.context.scene.abc3d_data.available_texts + ): + if butils.is_text_object_legit(text_properties.text_object): + if butils.is_or_has_parent( + bpy.context.active_object, text_properties.text_object + ): + return text_index + return -1 + + +def update_active_text_index(): + text_index = determine_active_text_index_from_selection() + if text_index != bpy.context.scene.abc3d_data.active_text_index: + bpy.context.scene.abc3d_data.active_text_index = text_index + + def detect_text(): - lock_depsgraph_updates(auto_unlock_s=-1) + lock_depsgraph_updates() scene = bpy.context.scene abc3d_data = scene.abc3d_data required_keys = [ @@ -2015,7 +1944,7 @@ def unlock_depsgraph_updates(): depsgraph_updates_locked -= 1 -def lock_depsgraph_updates(auto_unlock_s=1): +def lock_depsgraph_updates(auto_unlock_s=-1): global depsgraph_updates_locked depsgraph_updates_locked += 1 if auto_unlock_s >= 0: @@ -2059,6 +1988,7 @@ def on_depsgraph_update(scene, depsgraph): # must be a new thing, maybe manually created or so link_text_object_with_new_text_properties(u.id.original, scene) butils.clean_text_properties() + update_active_text_index() unlock_depsgraph_updates() diff --git a/butils.py b/butils.py index c24c891..28dbd0f 100644 --- a/butils.py +++ b/butils.py @@ -1184,6 +1184,14 @@ def get_text_properties(text_id, scene=None): return t return None +def get_text_properties_by_index(text_index, scene=None): + if scene is None: + scene = bpy.context.scene + abc3d_data = scene.abc3d_data + if text_index >= len(abc3d_data.available_texts): + return None + return abc3d_data.available_texts[text_index] + def duplicate( obj, From bb0a5a4a2ce96acb5c0c9b8c811ba34322cfbeec Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sat, 31 May 2025 19:57:11 +0200 Subject: [PATCH 095/103] add testing scripts --- testing_scripts/bezier_distance.py | 25 +++++++ testing_scripts/unload_glyphs.py | 115 +++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 testing_scripts/bezier_distance.py create mode 100644 testing_scripts/unload_glyphs.py diff --git a/testing_scripts/bezier_distance.py b/testing_scripts/bezier_distance.py new file mode 100644 index 0000000..5557a6b --- /dev/null +++ b/testing_scripts/bezier_distance.py @@ -0,0 +1,25 @@ +import bpy +from mathutils import * +from math import * +import abc3d.butils + +v = 0 +goal = 5.0 +step = 0.1 +speed = 1.0 + +C = bpy.context +obj = C.scene.objects['Cube'] +curve = C.scene.objects['BézierCurve'] + +m = curve.matrix + +def fun(distance): + obj.location = m @ abc3d.butils.calc_point_on_bezier_curve(curve, + distance, + output_tangent=True) + print(f"executed {distance}") + +while v < goal: + bpy.app.timers.register(lambda: fun(v), first_interval=(v * speed)) + v += step diff --git a/testing_scripts/unload_glyphs.py b/testing_scripts/unload_glyphs.py new file mode 100644 index 0000000..3802a58 --- /dev/null +++ b/testing_scripts/unload_glyphs.py @@ -0,0 +1,115 @@ +import bpy + +import abc3d +from abc3d import butils +from abc3d.common import Font + + +def get_text_properties_by_mom(mom): + scene = bpy.context.scene + abc3d_data = scene.abc3d_data + + for text_properties in abc3d_data.available_texts: + if mom == text_properties.text_object: + return text_properties + return None + + +def isolate_objects(objects): + for area in bpy.context.window.screen.areas: + if area.type == "VIEW_3D": + with bpy.context.temp_override( + selected_objects=list(objects), + area=area, + refgion=[region for region in area.regions if region.type == "WINDOW"][ + 0 + ], + screen=bpy.context.window.screen, + ): + # bpy.ops.view3d.view_selected() + bpy.ops.view3d.localview(frame_selected=True) + break + + +def main(): + # create a curve + bpy.ops.curve.primitive_bezier_curve_add( + radius=1, + enter_editmode=False, + align="WORLD", + location=(0, 0, 0), + scale=(1, 1, 1), + ) + # new curve is active object + mom = bpy.context.active_object + + # make sure + print(f"MOM: {mom.name}") + + fonts = Font.get_loaded_fonts_and_faces() + if len(fonts) == 0: + print("no fonts! what?") + return + + font_name = fonts[0][0] + face_name = fonts[0][1] + font = f"{font_name} {face_name}" + + isolate_objects([mom]) + + bpy.ops.abc3d.placetext( + font_name=font_name, + face_name=face_name, + font=font, + text="SOMETHING SOMETHING BROKEN ARMS", + letter_spacing=0, + font_size=1, + offset=0, + translation=(0, 0, 0), + orientation=(1.5708, 0, 0), + ) + + def change_text(font_name="", face_name="", text=""): + print(f"change_text to '{text}'") + text_properties = get_text_properties_by_mom(mom) + if font_name != "": + text_properties["font_name"] = font_name + if face_name != "": + text_properties["face_name"] = face_name + if text != "": + text_properties.text = text + else: + text_properties.text = text_properties.text + return None + + def unload(glyph_id): + print(f"unload glyph '{glyph_id}'") + butils.unload_unused_glyph(font_name, face_name, glyph_id) + return None + + def unload_all(): + print(f"unload glyph all unused glyphs") + butils.unload_unused_glyphs() + return None + + bpy.app.timers.register(lambda: change_text(text="SOMETHING"), first_interval=0) + bpy.app.timers.register(lambda: change_text(text="LOLSS"), first_interval=2) + bpy.app.timers.register(lambda: change_text(text="LOLAA"), first_interval=3) + bpy.app.timers.register(lambda: change_text(text="WHAT"), first_interval=4) + bpy.app.timers.register(lambda: change_text(text="LOL"), first_interval=5) + + bpy.app.timers.register(lambda: unload("A"), first_interval=10) + bpy.app.timers.register(lambda: unload_all(), first_interval=12) + + bpy.app.timers.register(lambda: change_text(text="LOLM"), first_interval=16) + bpy.app.timers.register(lambda: change_text(text="ZHE END"), first_interval=20) + + bpy.app.timers.register( + lambda: change_text(font_name="NM_Origin", face_name="Tender"), + first_interval=30, + ) + + bpy.app.timers.register(lambda: unload_all(), first_interval=42) + + +main() From 59edb2e786ec5001a648339291ac6fd0d66b810a Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sat, 31 May 2025 20:01:41 +0200 Subject: [PATCH 096/103] bump version to v0.0.11 --- README.md | 6 ++---- __init__.py | 2 +- common/utils.py | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 7316d62..ee15c47 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,8 @@ / ___ \| |_) | |___ ___) | |_| | /_/ \_\____/ \____|____/|____/ ``` -v0.0.10 +v0.0.11 -Convenience tool to work with 3D typography in Blender and Cinema4D. - -Install as you would normally install an addon. +Convenience addon to work with 3D typography in Blender and Cinema4D. Instructions for development in [CONTRIBUTING,md](./CONTRIBUTING.md). diff --git a/__init__.py b/__init__.py index c27a677..9ed5a48 100644 --- a/__init__.py +++ b/__init__.py @@ -16,7 +16,7 @@ from .common import Font, utils bl_info = { "name": "ABC3D", "author": "Jakob Schlötter, Studio Pointer*", - "version": (0, 0, 10), + "version": (0, 0, 11), "blender": (4, 1, 0), "location": "VIEW3D", "description": "Convenience addon for 3D fonts", diff --git a/common/utils.py b/common/utils.py index 743b7cd..4a036f8 100644 --- a/common/utils.py +++ b/common/utils.py @@ -8,7 +8,7 @@ def get_version_minor(): def get_version_patch(): - return 10 + return 11 def get_version_string(): From 14d1b7a160dada43e89357035a5a0ee40edbaa18 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sun, 1 Jun 2025 16:32:22 +0200 Subject: [PATCH 097/103] [fix] glyph receives text_id --- __init__.py | 54 +++++++++++++++++++++++------------------------------ butils.py | 2 ++ 2 files changed, 25 insertions(+), 31 deletions(-) diff --git a/__init__.py b/__init__.py index 9ed5a48..748d638 100644 --- a/__init__.py +++ b/__init__.py @@ -660,42 +660,34 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): # and bpy.context.object.select_get(): a_o = bpy.context.active_object if a_o is not None: - if f"{utils.prefix()}_text_id" in a_o: - text_index = a_o[f"{utils.prefix()}_text_id"] - return bpy.context.scene.abc3d_data.available_texts[text_index] - elif a_o.parent is not None and f"{utils.prefix()}_text_id" in a_o.parent: - text_index = a_o.parent[f"{utils.prefix()}_text_id"] - 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 - ): - return t + # if f"{utils.prefix()}_text_id" in a_o: + # text_id = a_o[f"{utils.prefix()}_text_id"] + # return butils.get_text_properties(text_id) + # # elif a_o.parent is not None and f"{utils.prefix()}_text_id" in a_o.parent: + # # text_id = a_o.parent[f"{utils.prefix()}_text_id"] + # # return butils.get_text_properties(text_id) + # 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 + ): + return t return None - def get_active_glyph_properties(self): + def get_active_glyph_properties(self, text_properties): + if text_properties is None: + return None 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 - ): - text_index = a_o[f"{utils.prefix()}_text_id"] + if f"{utils.prefix()}_glyph_index" in a_o: glyph_index = a_o[f"{utils.prefix()}_glyph_index"] - return bpy.context.scene.abc3d_data.available_texts[text_index].glyphs[ - glyph_index - ] + if len(text_properties.glyphs) <= glyph_index: + return None + return text_properties.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 - ): - for g in t.glyphs: - if butils.is_or_has_parent( - a_o, g.glyph_object, max_depth=4 - ): - return g + for g in text_properties.glyphs: + if butils.is_or_has_parent(a_o, g.glyph_object, max_depth=4): + return g return None @classmethod @@ -709,7 +701,7 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): layout = self.layout props = self.get_active_text_properties() - glyph_props = self.get_active_glyph_properties() + glyph_props = self.get_active_glyph_properties(props) if props is None or props.text_object is None: # this should not happen diff --git a/butils.py b/butils.py index 28dbd0f..fba0ef4 100644 --- a/butils.py +++ b/butils.py @@ -1301,6 +1301,8 @@ def transfer_text_object_to_text_properties( transfer_glyph_object_to_glyph_properties(glyph_object, glyph_properties) glyph_properties["glyph_object"] = glyph_object glyph_properties["glyph_index"] = glyph_index + glyph_properties["text_id"] = text_properties.text_id + glyph_object["text_id"] = text_properties.text_id inner_node = None for c in glyph_object.children: if c.name.startswith(f"{glyph_id}_mesh"): From 7de8fcc5d115510c3857aa81fff44453564793ea Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Wed, 4 Jun 2025 14:47:09 +0200 Subject: [PATCH 098/103] refactor ensure glyphs + alternates --- __init__.py | 85 ++++++++++-- butils.py | 353 ++++++++++++++++++++++++++++++++----------------- common/Font.py | 23 +++- 3 files changed, 332 insertions(+), 129 deletions(-) diff --git a/__init__.py b/__init__.py index 748d638..8d9a59d 100644 --- a/__init__.py +++ b/__init__.py @@ -143,13 +143,40 @@ class ABC3D_glyph_properties(bpy.types.PropertyGroup): t = butils.get_text_properties(self.text_id) if t is not None: butils.set_text_on_curve(t) + return None + + def alternate_get_callback(self): + return self["alternate"] if "alternate" in self else 0 + + def alternate_set_callback(self, value): + min_value = 0 + new_value = max(value, min_value) + + if self.text_id >= 0: + text_properties = butils.get_text_properties(self.text_id) + max_value = ( + len( + Font.get_glyphs( + text_properties.font_name, + text_properties.face_name, + self.glyph_id, + ) + ) + - 1 + ) + new_value = min(new_value, max_value) + + self["alternate"] = new_value + return None glyph_id: bpy.props.StringProperty(maxlen=1) text_id: bpy.props.IntProperty( default=-1, ) alternate: bpy.props.IntProperty( - default=-1, + default=0, # also change in alternate_get_callback + get=alternate_get_callback, + set=alternate_set_callback, update=update_callback, ) glyph_object: bpy.props.PointerProperty(type=bpy.types.Object) @@ -267,6 +294,7 @@ class ABC3D_data(bpy.types.PropertyGroup): available_texts: bpy.props.CollectionProperty( type=ABC3D_text_properties, name="Available texts" ) + texts: bpy.props.CollectionProperty(type=ABC3D_text_properties, name="texts") def active_text_index_update(self, context): lock_depsgraph_updates() @@ -713,6 +741,22 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): layout.label(text="props.text_object is none") return + # TODO: put this at a better place + # here we set the font if it is not correct + # this is a fix for a UI glitch, perhaps it could be fixed + # rather where it is not set properly + # if ( + # butils.get_key("font_name") in props.text_object + # and butils.get_key("face_name") in props.text_object + # ): + # font = f"{props.text_object[butils.get_key('font_name')]} {props.text_object[butils.get_key('face_name')]}" + # if font != props.font: + # + # def setfont(): + # props.font = font + # + # butils.run_in_main_thread(setfont) + # layout.label(text=f"Mom: {props.text_object.name}") layout.row().prop(props, "font") layout.row().prop(props, "text") @@ -729,8 +773,26 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): if glyph_props is None: return box = layout.box() - box.label(text=f"{glyph_props.glyph_id}") + box.label(text=f"selected character: {glyph_props.glyph_id}") box.row().prop(glyph_props, "letter_spacing") + # if True: + # font_name = props.font_name + # face_name = props.face_name + # glyph_id = glyph_props.glyph_id + # glyphs_n = len(Font.get_glyphs(font_name, face_name, glyph_id)) + # glyph_props.alternate.hard_min = -1 + # glyph_props.alternate.hard_max = glyphs_n - 1 + n_alternates = len( + Font.get_glyphs( + props.font_name, + props.face_name, + glyph_props.glyph_id, + ) + ) + if n_alternates > 1: + box.row().prop(glyph_props, "alternate", text=f"alternate ({n_alternates})") + # if glyph_props.glyph_object.preview is not None: + # box.row().template_preview(glyph_props.glyph_object.preview.icon_id) class ABC3D_OT_RefreshAvailableFonts(bpy.types.Operator): @@ -1586,9 +1648,10 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): def do_autodetect_names(self, name: str): ifxsplit = name.split("_") if len(ifxsplit) < 4: - print(f"name could not be autodetected {name}") - print("split:") - print(ifxsplit) + print( + f"{utils.prefix()}::CreateFontFromObjects: name could not be autodetected {name}" + ) + print(f"{utils.prefix()}::CreateFontFromObjects: split: {ifxsplit=}") return self.font_name, self.face_name detected_font_name = f"{ifxsplit[1]}_{ifxsplit[2]}" detected_face_name = ifxsplit[3] @@ -1663,9 +1726,11 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): row.label(text=f"{k} → {Font.known_misspellings[k]}{character}") def execute(self, context): - print(f"executing {self.bl_idname}") + print(f"{utils.prefix()}::CreateFontFromObjects: executing {self.bl_idname}") if len(context.selected_objects) == 0: - print(f"cancelled {self.bl_idname} - no objects selected") + print( + f"{utils.prefix()}::CreateFontFromObjects: cancelled {self.bl_idname} - no objects selected" + ) return {"CANCELLED"} global shared scene = bpy.context.scene @@ -1682,7 +1747,7 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): currentObjects = [] for o in context.selected_objects: if o.name not in currentObjects: - print(f"processing {o.name}") + print(f"{utils.prefix()}::CreateFontFromObjects: processing {o.name}") process_object = True if self.autodetect_names: font_name, face_name = self.do_autodetect_names(o.name) @@ -1719,7 +1784,9 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): f.face_name = face_name else: - print(f"import warning: did not understand glyph {name}") + print( + f"{utils.prefix()}::CreateFontFromObjects: import warning: did not understand glyph {name}" + ) self.report({"INFO"}, f"did not understand glyph {name}") return {"FINISHED"} diff --git a/butils.py b/butils.py index fba0ef4..e7aa72d 100644 --- a/butils.py +++ b/butils.py @@ -663,7 +663,7 @@ def clean_fontcollection(fontcollection=None): fontcollection = bpy.data.collections.get("ABC3D") if fontcollection is None: print( - f"{utils.prefix()}::clean_fontcollection: failed beacause fontcollection is none" + f"{utils.prefix()}::clean_fontcollection: failed because fontcollection is none" ) return False @@ -1011,9 +1011,11 @@ def predict_actual_text(text_properties): ) 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, "") + C = c.swapcase() + if C in AVAILABILITY.missing: + t_text = t_text.replace(c, "") + else: + t_text = t_text.replace(c, C) return t_text @@ -1184,6 +1186,7 @@ def get_text_properties(text_id, scene=None): return t return None + def get_text_properties_by_index(text_index, scene=None): if scene is None: scene = bpy.context.scene @@ -1299,10 +1302,7 @@ def transfer_text_object_to_text_properties( glyph_properties = text_properties.glyphs.add() transfer_glyph_object_to_glyph_properties(glyph_object, glyph_properties) - glyph_properties["glyph_object"] = glyph_object - glyph_properties["glyph_index"] = glyph_index - glyph_properties["text_id"] = text_properties.text_id - glyph_object["text_id"] = text_properties.text_id + # glyph_properties["glyph_object"] = glyph_object inner_node = None for c in glyph_object.children: if c.name.startswith(f"{glyph_id}_mesh"): @@ -1311,6 +1311,9 @@ def transfer_text_object_to_text_properties( fail_after_all = True pass glyph_properties["glyph_object"] = glyph_object + glyph_properties["glyph_index"] = glyph_index + glyph_properties["text_id"] = text_properties.text_id + glyph_object["text_id"] = text_properties.text_id if not fail_after_all: found_reconstructable_glyphs = True @@ -1421,10 +1424,21 @@ def transfer_glyph_object_to_glyph_properties(glyph_object, glyph_properties): glyph_properties["text_id"] = glyph_object[get_key("text_id")] +def get_text_difference_index(text_a, text_b): + len_a = len(text_a) + len_b = len(text_b) + len_min = min(len_a, len_b) + len_max = max(len_a, len_b) + for i in range(0, len_max): + if i >= len_min or text_a[i] != text_b[i]: + return i + return False + + def would_regenerate(text_properties): predicted_text = predict_actual_text(text_properties) if text_properties.actual_text != predicted_text: - return True + return get_text_difference_index(text_properties.actual_text, predicted_text) if len(text_properties.glyphs) == 0: return True @@ -1476,6 +1490,7 @@ def is_or_has_parent(o, parent, if_is_parent=True, max_depth=10): def parent_to_curve(o, c): + # https://projects.blender.org/blender/blender/issues/100661 o.parent_type = "OBJECT" o.parent = c # o.matrix_parent_inverse = c.matrix_world.inverted() @@ -1491,6 +1506,184 @@ def parent_to_curve(o, c): o.matrix_parent_inverse.translation = p * -1.0 +def get_original_glyph(text_properties, glyph_properties): + glyph_tmp = Font.get_glyph( + text_properties.font_name, + text_properties.face_name, + glyph_properties.glyph_id, + glyph_properties.alternate, + ) + if glyph_tmp is None: + return None + return glyph_tmp.original + + +def ensure_glyph_object(text_properties, glyph_properties): + glyph_index = glyph_properties["glyph_index"] + # First, let's see if there was ever a glyph object constructed + if ( + glyph_properties.glyph_object is None + or not isinstance(glyph_properties.glyph_object, bpy_types.Object) + or not is_glyph_object(glyph_properties.glyph_object) + ): + # we do need a text_object though + # if there is not, let's give up for this iteration + if not isinstance(text_properties.text_object, bpy_types.Object): + print( + f"{utils.prefix()}::ensure_glyph_object: failed! text object is not an object" + ) + return False + + outer_node = bpy.data.objects.new(f"{glyph_properties.glyph_id}", None) + inner_node = bpy.data.objects.new( + f"{glyph_properties.glyph_id}_mesh", + get_original_glyph(text_properties, glyph_properties).data, + ) + transfer_properties_to_glyph_object( + text_properties, glyph_properties, outer_node + ) + + # Add into the scene. + text_properties.text_object.users_collection[0].objects.link(outer_node) + text_properties.text_object.users_collection[0].objects.link(inner_node) + + # Parenting is hard. + 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, text_properties.text_object) + + # outer_node["inner_node"] = bpy.types.PointerProperty(inner_node) + # for some funny reason we cannot set 'glyph_object' by key, but need to set the attribute + glyph_properties.glyph_object = outer_node + outer_node[f"{utils.prefix()}_glyph_index"] = glyph_index + else: + outer_node = glyph_properties.glyph_object + outer_node[f"{utils.prefix()}_glyph_index"] = glyph_index + + # we might just want to update the data + # imagine a different font, letter or alternate + # this way we keep all manual transforms + if ( + glyph_properties.glyph_object[get_key("glyph_id")] != glyph_properties.glyph_id + or glyph_properties.glyph_object[get_key("alternate")] + != glyph_properties.alternate + or glyph_properties.glyph_object[get_key("font_name")] + != text_properties.font_name + or glyph_properties.glyph_object[get_key("face_name")] + != text_properties.face_name + ): + + inner_node = None + old_font_name = glyph_properties.glyph_object[get_key("font_name")] + old_face_name = glyph_properties.glyph_object[get_key("face_name")] + old_face = Font.get_font_face(old_font_name, old_face_name) + face = Font.get_font_face(text_properties.font_name, text_properties.face_name) + ratio = old_face.unit_factor / face.unit_factor + # try: + # inner_node = glyph_properties["inner_node"].original + # inner_node.location = inner_node.location * ratio + # except KeyError: + old_glyph_id = glyph_properties.glyph_object[get_key("glyph_id")] + for c in glyph_properties.glyph_object.children: + if c.name.startswith(f"{old_glyph_id}_mesh"): + inner_node = c + inner_node.location = inner_node.location * ratio + inner_node.name = f"{glyph_properties.glyph_id}_mesh" + # outer_node["inner_node"] = bpy.types.PointerProperty(inner_node) + if inner_node is None: + print(f"{utils.prefix()}::ensure_glyph_object: failed! no inner_node found") + return False + inner_node.data = get_original_glyph(text_properties, glyph_properties).data + glyph_properties.glyph_object[get_key("glyph_id")] = glyph_properties.glyph_id + glyph_properties.glyph_object[get_key("alternate")] = glyph_properties.alternate + glyph_properties.glyph_object[get_key("font_name")] = text_properties.font_name + glyph_properties.glyph_object[get_key("face_name")] = text_properties.face_name + + glyph_properties.glyph_object.hide_set(True) + + return True + + +def ensure_glyphs(text_properties, predicted_text: str): + + ######### REQUIREMENTS + + # turns out this is not a requirement + # and can be a case we want to tackle + # + # if not text_properties.get("glyphs"): + # ShowMessageBox( + # title="text_properties has no glyphs", message="well, what I said" + # ) + # return False + + ######### SETUP + + n_glyphs = len(text_properties.glyphs) + n_predicted = len(predicted_text) + + ########## ENSURE AMOUNT + + if n_glyphs == n_predicted: + # same amount of glyphs + # this is the most common case + # don't do anything + pass + + elif n_glyphs > n_predicted: + # more glyphs than predicted + # it's a shorter word, or letters were deleted + count = n_glyphs - n_predicted + for i in range(0, count): + reverse_i = n_glyphs - (i + 1) + # let's attempt to remove the glyph_object first + # so we avoid dangling data + if isinstance( + text_properties.glyphs[reverse_i].glyph_object, bpy_types.Object + ): + # bam! + completely_delete_objects( + [text_properties.glyphs[reverse_i].glyph_object] + ) + # else: + # # nothing to do, if there is no blender object + # # possibly we could do a 'del', but we can also + # # just comment out the whole conditional fork + # pass + + # now that blender data is gone, we can remove the glyph + text_properties.glyphs.remove(reverse_i) + + elif n_glyphs < n_predicted: + # less glyphs than predicted + # it's a longer word, or letters were added + while n_glyphs < n_predicted: + glyph_id = predicted_text[n_glyphs] + glyph_properties = text_properties.glyphs.add() + glyph_properties["glyph_id"] = predicted_text[n_glyphs] + glyph_properties["glyph_index"] = n_glyphs + glyph_properties["text_id"] = text_properties.text_id + glyph_properties["letter_spacing"] = 0 + n_glyphs += 1 + + ######### ENSURE VALUES + + for i, glyph_properties in enumerate(text_properties.glyphs): + glyph_properties["glyph_index"] = i + glyph_properties["text_id"] = text_properties.text_id + glyph_properties["glyph_id"] = predicted_text[i] + if not ensure_glyph_object(text_properties, glyph_properties): + print(f"{utils.prefix()}::ensure_glyphs: could not ensure glyph_object") + + return True + + +# C.scene.abc3d_data.available_texts[0] +# import abc3d +# abc3d.butils.ensure_glyphs(C.scene.abc3d_data.available_texts[0], "whatever") + + def set_text_on_curve( text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, can_regenerate=False ): @@ -1519,27 +1712,8 @@ def set_text_on_curve( 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 - # - # use_path messes with parenting - # however, we need it for follow_path - # https://projects.blender.org/blender/blender/issues/100661 - # previous_use_path = mom.data.use_path - # if distribution_type == "CALCULATE": - # mom.data.use_path = False - # elif distribution_type == "FOLLOW_PATH": - # mom.data.use_path = True - - regenerate = can_regenerate and would_regenerate(text_properties) - - # if we regenerate.... delete objects - if regenerate and text_properties.get("glyphs"): - glyph_objects = [g["glyph_object"] for g in text_properties["glyphs"]] - completely_delete_objects(glyph_objects, True) - text_properties.glyphs.clear() - - transfer_text_properties_to_text_object(text_properties, mom) + predicted_text = predict_actual_text(text_properties) + ensure_glyphs(text_properties, predicted_text) curve_length = get_curve_length(mom) advance = text_properties.offset @@ -1549,6 +1723,9 @@ def set_text_on_curve( previous_spline_index = -1 actual_text = "" + # we need to iterate over the original text, as we want commands + # however, ideally it could be an array of glyphs, commands and spaces + # now we need to handle non existing characters etc everytime in the loop for i, c in enumerate(text_properties.text): face = Font.get_font_face(text_properties.font_name, text_properties.face_name) scalor = face.unit_factor * text_properties.font_size @@ -1572,90 +1749,31 @@ def set_text_on_curve( spline_index = 0 - ############### GET GLYPH + ############### HANDLE SPACES - glyph_tmp = Font.get_glyph( - text_properties.font_name, text_properties.face_name, glyph_id, -1 - ) - if glyph_tmp is None: + if glyph_id not in predicted_text: space_width = Font.is_space(glyph_id) if space_width: advance = advance + space_width * text_properties.font_size - continue - - message = f"Glyph not found for font_name='{text_properties.font_name}' face_name='{text_properties.face_name}' glyph_id='{glyph_id}'" - replaced = False - if glyph_id.isalpha(): - possible_replacement = glyph_id.swapcase() - glyph_tmp = Font.get_glyph( - text_properties.font_name, - text_properties.face_name, - possible_replacement, - -1, - ) - if glyph_tmp is not None: - message = message + f" (replaced with '{possible_replacement}')" - replaced = True - - if can_regenerate: - ShowMessageBox( - title="Glyph replaced" if replaced else "Glyph missing", - icon="INFO" if replaced else "ERROR", - message=message, - prevent_repeat=True, - ) - if not replaced: - continue - - glyph = glyph_tmp.original + continue ############### GLYPH PROPERTIES - glyph_properties = ( - text_properties.glyphs[glyph_index] - if not regenerate - else text_properties.glyphs.add() - ) + glyph_properties = text_properties.glyphs[glyph_index] + # ensure_glyph_object(text_properties, glyph_properties) - if regenerate: - glyph_properties["glyph_id"] = glyph_id - glyph_properties["text_id"] = text_properties.text_id - glyph_properties["letter_spacing"] = 0 - actual_text += glyph_id + ############### ACTUAL TEXT + + actual_text += glyph_id ############### NODE SCENE MANAGEMENT - inner_node = None - outer_node = None - 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 - ) - - # 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 = outer_node - inner_node.matrix_parent_inverse = outer_node.matrix_world.inverted() - parent_to_curve(outer_node, mom) - outer_node.hide_set(True) - - glyph_properties["glyph_object"] = outer_node - outer_node[f"{utils.prefix()}_glyph_index"] = glyph_index - else: - outer_node = glyph_properties.glyph_object - outer_node[f"{utils.prefix()}_glyph_index"] = glyph_index - for c in outer_node.children: - if c.name.startswith(f"{glyph_id}_mesh"): - inner_node = c + # outsourced to ensure_glyph_object ############### TRANSFORMS + glyph = get_original_glyph(text_properties, glyph_properties) + # origins could be shifted # so we need to apply a pre_advance glyph_pre_advance, glyph_post_advance = get_glyph_prepost_advances(glyph) @@ -1683,14 +1801,12 @@ def set_text_on_curve( outer_node.constraints["Follow Path"].up_axis = "UP_Y" spline_index = 0 elif distribution_type == "CALCULATE": - previous_outer_node_rotation_mode = None - previous_inner_node_rotation_mode = None - if outer_node.rotation_mode != "QUATERNION": - outer_node.rotation_mode = "QUATERNION" - previous_outer_node_rotation_mode = outer_node.rotation_mode - if inner_node.rotation_mode != "QUATERNION": - inner_node.rotation_mode = "QUATERNION" - previous_inner_node_rotation_mode = inner_node.rotation_mode + previous_glyph_object_rotation_mode = None + if glyph_properties.glyph_object.rotation_mode != "QUATERNION": + previous_glyph_object_rotation_mode = ( + glyph_properties.glyph_object.rotation_mode + ) + glyph_properties.glyph_object.rotation_mode = "QUATERNION" # get info from bezier location, tangent, spline_index = calc_point_on_bezier_curve( @@ -1702,7 +1818,9 @@ def set_text_on_curve( is_newline = True # position - outer_node.location = location + text_properties.translation + glyph_properties.glyph_object.location = ( + location + text_properties.translation + ) # orientation / rotation mask = [0] @@ -1720,21 +1838,21 @@ def set_text_on_curve( q = mathutils.Quaternion() q.rotate(text_properties.orientation) - outer_node.rotation_quaternion = ( + glyph_properties.glyph_object.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: - # outer_node.rotation_quaternion = (mom.matrix_world.inverted().to_3x3() @ motor[0].to_3x3() @ q.to_matrix()).to_quaternion() + # glyph_properties.glyph_object.rotation_quaternion = (mom.matrix_world.inverted().to_3x3() @ motor[0].to_3x3() @ q.to_matrix()).to_quaternion() # # scale - outer_node.scale = (scalor, scalor, scalor) + glyph_properties.glyph_object.scale = (scalor, scalor, scalor) - if previous_outer_node_rotation_mode: - outer_node.rotation_mode = previous_outer_node_rotation_mode - if previous_inner_node_rotation_mode: - inner_node.rotation_mode = previous_inner_node_rotation_mode + if previous_glyph_object_rotation_mode: + glyph_properties.glyph_object.rotation_mode = ( + previous_glyph_object_rotation_mode + ) # outer_node.hide_viewport = True @@ -1805,8 +1923,7 @@ def set_text_on_curve( glyph_index += 1 previous_spline_index = spline_index - if regenerate: - text_properties["actual_text"] = actual_text + text_properties["actual_text"] = actual_text return True diff --git a/common/Font.py b/common/Font.py index d68518c..aebf702 100644 --- a/common/Font.py +++ b/common/Font.py @@ -266,7 +266,13 @@ def get_glyphs(font_name, face_name, glyph_id): return glyphs_for_id -def get_glyph(font_name, face_name, glyph_id, alternate=0): +def get_glyph( + font_name: str, + face_name: str, + glyph_id: str, + alternate: int = 0, + alternate_tolerant: bool = True, +): """add_glyph adds a glyph to a FontFace it creates the :class:`Font` and :class:`FontFace` if it does not exist yet @@ -276,6 +282,10 @@ def get_glyph(font_name, face_name, glyph_id, alternate=0): :type face_name: str :param glyph_id: The ``glyph_id`` from the glyph you want :type glyph_id: str + :param alternate: The ``alternate`` from the glyph you want + :type alternate: int + :param alternate_tolerant: Fetch an existing alternate if requested is out of bounds + :type glyph_id: bool ... :return: returns the glyph object, or ``None`` if it does not exist :rtype: `Object` @@ -283,12 +293,21 @@ def get_glyph(font_name, face_name, glyph_id, alternate=0): glyphs = get_glyphs(font_name, face_name, glyph_id) - if len(glyphs) <= alternate or len(glyphs) == 0: + if len(glyphs) == 0: print( f"ABC3D::get_glyph: font({font_name}) face({face_name}) glyph({glyph_id})[{alternate}] not found" ) return None + if len(glyphs) <= alternate: + if alternate_tolerant: + alternate = 0 + else: + print( + f"ABC3D::get_glyph: font({font_name}) face({face_name}) glyph({glyph_id})[{alternate}] not found" + ) + return None + return glyphs[alternate] From 58e0df34275ce4373fc96b675b0984e50d8ed848 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Wed, 4 Jun 2025 21:28:08 +0200 Subject: [PATCH 099/103] first step spline animation --- __init__.py | 14 ++++++ butils.py | 142 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 152 insertions(+), 4 deletions(-) diff --git a/__init__.py b/__init__.py index 8d9a59d..a4021ab 100644 --- a/__init__.py +++ b/__init__.py @@ -2040,13 +2040,27 @@ def on_depsgraph_update(scene, depsgraph): elif butils.is_text_object_legit(u.id.original): # must be duplicate link_text_object_with_new_text_properties(u.id.original, scene) + # butils.prepare_text( + # text_properties.font_name, + # text_properties.face_name, + # text_properties.text, + # ) elif ( butils.is_text_object_legit(u.id.original) and len(u.id.original.users_collection) > 0 ): # must be a new thing, maybe manually created or so link_text_object_with_new_text_properties(u.id.original, scene) + # butils.prepare_text( + # text_properties.font_name, + # text_properties.face_name, + # text_properties.text, + # ) butils.clean_text_properties() + try: + butils.set_text_on_curve(text_properties) + except: + pass update_active_text_index() unlock_depsgraph_updates() diff --git a/butils.py b/butils.py index e7aa72d..67367df 100644 --- a/butils.py +++ b/butils.py @@ -217,6 +217,118 @@ def calc_bezier_length(bezier_point_1, bezier_point_2, resolution=20): return length +def get_hook_modifiers(blender_object: bpy.types.Object): + return [m for m in blender_object.modifiers if m.type == "HOOK"] + + +class BezierSplinePoint: + def __init__( + self, + co: mathutils.Vector, + handle_left: mathutils.Vector, + handle_right: mathutils.Vector, + ): + self.co: mathutils.Vector = co + self.handle_left: mathutils.Vector = handle_left + self.handle_right: mathutils.Vector = handle_right + + +class BezierSpline: + def __init__( + self, + n: int, + use_cyclic_u: bool, + resolution_u: int, + ): + self.bezier_points = [BezierSplinePoint] * n + self.use_cyclic_u: int = use_cyclic_u + self.resolution_u: int = resolution_u + self.beziers: [] + self.lengths: [float] + self.total_length: float + + def calc_length(self, resolution) -> float: + # ignore resolution when accessing length to imitate blender function + print(f"{self.total_length=}") + return self.total_length + + +class BezierData: + def __init__(self, n): + self.splines = [BezierSpline] * n + + +class BezierCurve: + def __init__(self, blender_curve: bpy.types.Object, resolution_factor=1.0): + self.data = BezierData(len(blender_curve.data.splines)) + i = 0 + hooks = get_hook_modifiers(blender_curve) + print(f"{blender_curve.name=} =============================================") + for si, blender_spline in enumerate(blender_curve.data.splines): + self.data.splines[si] = BezierSpline( + len(blender_spline.bezier_points), + blender_spline.use_cyclic_u, + blender_spline.resolution_u, + ) + for pi, blender_bezier_point in enumerate(blender_spline.bezier_points): + self.data.splines[si].bezier_points[pi] = BezierSplinePoint( + blender_bezier_point.co, + blender_bezier_point.handle_left, + blender_bezier_point.handle_right, + ) + print(pi) + for hook in hooks: + hook_co = False + hook_handle_left = False + hook_handle_right = False + for vi in hook.vertex_indices: + if vi == i * 3: + hook_handle_left = True + elif vi == i * 3 + 1: + hook_co = True + elif vi == i * 3 + 2: + hook_handle_right = True + if hook_co: + location = ( + blender_curve.matrix_world.inverted() + @ hook.object.matrix_world.translation + ) + print(f"co {location=}") + self.data.splines[si].bezier_points[pi].co = ( + self.data.splines[si] + .bezier_points[pi] + .co.lerp(location, hook.strength) + ) + # if hook_handle_left: + # location = ( + # hook.object.matrix_world.translation + # - blender_curve.matrix_world.translation + # ) + mathutils.Vector((-1, 0, 0)) + # print(f"handle_left {location=}") + # self.data.splines[si].bezier_points[pi].handle_left = ( + # self.data.splines[si] + # .bezier_points[pi] + # .handle_left.lerp(location, hook.strength) + # ) + # if hook_handle_right: + # location = ( + # hook.object.matrix_world.translation + # - blender_curve.matrix_world.translation + # ) + # self.data.splines[si].bezier_points[pi].handle_right = ( + # self.data.splines[si] + # .bezier_points[pi] + # .handle_right.lerp(location, hook.strength) + # ) + i += 1 + ( + self.data.splines[si].beziers, + self.data.splines[si].lengths, + self.data.splines[si].total_length, + ) = get_real_beziers_and_lengths(self.data.splines[si], resolution_factor) + print(f"total length {self.data.splines[si].total_length}") + + def get_real_beziers_and_lengths(bezier_spline_obj, resolution_factor): beziers = [] lengths = [] @@ -277,8 +389,14 @@ 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) + if not isinstance(bezier_spline_obj, BezierSpline) + else ( + bezier_spline_obj.beziers, + bezier_spline_obj.lengths, + bezier_spline_obj.total_length, + ) ) travel_point = calc_point_on_bezier(beziers[0][1], beziers[0][0], 0.001) travel = travel_point.normalized() * distance @@ -291,8 +409,14 @@ 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) + if not isinstance(bezier_spline_obj, BezierSpline) + else ( + bezier_spline_obj.beziers, + bezier_spline_obj.lengths, + bezier_spline_obj.total_length, + ) ) iterated_distance = 0 @@ -336,6 +460,8 @@ def calc_point_on_bezier_curve( output_spline_index=False, resolution_factor=1.0, ): + # bezier_curve = BezierCurve(bezier_curve_obj) + # curve = bezier_curve.data curve = bezier_curve_obj.data # Loop through all splines in the curve @@ -343,6 +469,7 @@ def calc_point_on_bezier_curve( for i, spline in enumerate(curve.splines): resolution = int(spline.resolution_u * resolution_factor) length = spline.calc_length(resolution=resolution) + print(f"{utils.LINE()} {length=}") if total_length + length > distance or i == len(curve.splines) - 1: if output_spline_index and output_tangent: # return value from c_p_o_b_s is a tuple @@ -1704,7 +1831,11 @@ def set_text_on_curve( # global lock_depsgraph_update_n_times # starttime = time.perf_counter_ns() + if text_properties is None: + return False mom = text_properties.text_object + if mom is None: + return False if mom.type != "CURVE": return False if len(mom.users_collection) < 1: @@ -1812,6 +1943,9 @@ def set_text_on_curve( location, tangent, spline_index = calc_point_on_bezier_curve( mom, applied_advance, True, True ) + # location, tangent, spline_index = calc_point_on_bezier_curve( + # mom_hooked, applied_advance, True, True + # ) # check if we are on a new line if spline_index != previous_spline_index: From e95266afc94a2b1769c9ef5e00094f14f25957cb Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Thu, 5 Jun 2025 10:56:33 +0200 Subject: [PATCH 100/103] [fix] transfer text properties --- butils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/butils.py b/butils.py index 67367df..bb3bc1d 100644 --- a/butils.py +++ b/butils.py @@ -1802,6 +1802,9 @@ def ensure_glyphs(text_properties, predicted_text: str): glyph_properties["glyph_id"] = predicted_text[i] if not ensure_glyph_object(text_properties, glyph_properties): print(f"{utils.prefix()}::ensure_glyphs: could not ensure glyph_object") + transfer_text_properties_to_text_object( + text_properties, text_properties.text_object + ) return True From f02f8fc2f0283337c548e4d61245aaeafa775a74 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Thu, 5 Jun 2025 10:57:53 +0200 Subject: [PATCH 101/103] depsgraph update set text this updates on animation --- __init__.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/__init__.py b/__init__.py index a4021ab..697e857 100644 --- a/__init__.py +++ b/__init__.py @@ -2036,31 +2036,21 @@ def on_depsgraph_update(scene, depsgraph): if text_properties is not None: if text_properties.text_object == u.id.original: # nothing to do + try: + butils.set_text_on_curve(text_properties) + except: + pass pass elif butils.is_text_object_legit(u.id.original): # must be duplicate link_text_object_with_new_text_properties(u.id.original, scene) - # butils.prepare_text( - # text_properties.font_name, - # text_properties.face_name, - # text_properties.text, - # ) elif ( butils.is_text_object_legit(u.id.original) and len(u.id.original.users_collection) > 0 ): # must be a new thing, maybe manually created or so link_text_object_with_new_text_properties(u.id.original, scene) - # butils.prepare_text( - # text_properties.font_name, - # text_properties.face_name, - # text_properties.text, - # ) butils.clean_text_properties() - try: - butils.set_text_on_curve(text_properties) - except: - pass update_active_text_index() unlock_depsgraph_updates() From cd99362bb1c9b3ceef9f334dff224e18d516120c Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Thu, 5 Jun 2025 10:59:14 +0200 Subject: [PATCH 102/103] [feature] spline animation --- butils.py | 105 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 59 insertions(+), 46 deletions(-) diff --git a/butils.py b/butils.py index bb3bc1d..30b0a47 100644 --- a/butils.py +++ b/butils.py @@ -221,26 +221,26 @@ def get_hook_modifiers(blender_object: bpy.types.Object): return [m for m in blender_object.modifiers if m.type == "HOOK"] -class BezierSplinePoint: +class HookBezierSplinePoint: def __init__( self, - co: mathutils.Vector, handle_left: mathutils.Vector, + co: mathutils.Vector, handle_right: mathutils.Vector, ): - self.co: mathutils.Vector = co - self.handle_left: mathutils.Vector = handle_left - self.handle_right: mathutils.Vector = handle_right + self.handle_left: mathutils.Vector = mathutils.Vector(handle_left) + self.co: mathutils.Vector = mathutils.Vector(co) + self.handle_right: mathutils.Vector = mathutils.Vector(handle_right) -class BezierSpline: +class HookBezierSpline: def __init__( self, n: int, use_cyclic_u: bool, resolution_u: int, ): - self.bezier_points = [BezierSplinePoint] * n + self.bezier_points = [HookBezierSplinePoint] * n self.use_cyclic_u: int = use_cyclic_u self.resolution_u: int = resolution_u self.beziers: [] @@ -249,34 +249,31 @@ class BezierSpline: def calc_length(self, resolution) -> float: # ignore resolution when accessing length to imitate blender function - print(f"{self.total_length=}") return self.total_length -class BezierData: +class HookBezierData: def __init__(self, n): - self.splines = [BezierSpline] * n + self.splines = [HookBezierSpline] * n -class BezierCurve: +class HookBezierCurve: def __init__(self, blender_curve: bpy.types.Object, resolution_factor=1.0): - self.data = BezierData(len(blender_curve.data.splines)) + self.data = HookBezierData(len(blender_curve.data.splines)) i = 0 hooks = get_hook_modifiers(blender_curve) - print(f"{blender_curve.name=} =============================================") for si, blender_spline in enumerate(blender_curve.data.splines): - self.data.splines[si] = BezierSpline( + self.data.splines[si] = HookBezierSpline( len(blender_spline.bezier_points), blender_spline.use_cyclic_u, blender_spline.resolution_u, ) for pi, blender_bezier_point in enumerate(blender_spline.bezier_points): - self.data.splines[si].bezier_points[pi] = BezierSplinePoint( - blender_bezier_point.co, + self.data.splines[si].bezier_points[pi] = HookBezierSplinePoint( blender_bezier_point.handle_left, + blender_bezier_point.co, blender_bezier_point.handle_right, ) - print(pi) for hook in hooks: hook_co = False hook_handle_left = False @@ -293,40 +290,57 @@ class BezierCurve: blender_curve.matrix_world.inverted() @ hook.object.matrix_world.translation ) - print(f"co {location=}") self.data.splines[si].bezier_points[pi].co = ( self.data.splines[si] .bezier_points[pi] .co.lerp(location, hook.strength) ) - # if hook_handle_left: - # location = ( - # hook.object.matrix_world.translation - # - blender_curve.matrix_world.translation - # ) + mathutils.Vector((-1, 0, 0)) - # print(f"handle_left {location=}") - # self.data.splines[si].bezier_points[pi].handle_left = ( - # self.data.splines[si] - # .bezier_points[pi] - # .handle_left.lerp(location, hook.strength) - # ) - # if hook_handle_right: - # location = ( - # hook.object.matrix_world.translation - # - blender_curve.matrix_world.translation - # ) - # self.data.splines[si].bezier_points[pi].handle_right = ( - # self.data.splines[si] - # .bezier_points[pi] - # .handle_right.lerp(location, hook.strength) - # ) + if hook_handle_left: + location_left = location + ( + self.data.splines[si].bezier_points[pi].co + - self.data.splines[si].bezier_points[pi].handle_left + ) + self.data.splines[si].bezier_points[pi].handle_left = ( + self.data.splines[si] + .bezier_points[pi] + .co.lerp(location_left, hook.strength) + ) + if hook_handle_right: + location_right = location + ( + self.data.splines[si].bezier_points[pi].co + - self.data.splines[si].bezier_points[pi].handle_right + ) + self.data.splines[si].bezier_points[pi].handle_right = ( + self.data.splines[si] + .bezier_points[pi] + .co.lerp(location, hook.strength) + ) + elif hook_handle_left: + location = ( + blender_curve.matrix_world.inverted() + @ hook.object.matrix_world.translation + ) + self.data.splines[si].bezier_points[pi].handle_left = ( + self.data.splines[si] + .bezier_points[pi] + .handle_left.lerp(location, hook.strength) + ) + elif hook_handle_right: + location = ( + blender_curve.matrix_world.inverted() + @ hook.object.matrix_world.translation + ) + self.data.splines[si].bezier_points[pi].handle_right = ( + self.data.splines[si] + .bezier_points[pi] + .handle_right.lerp(location, hook.strength) + ) i += 1 ( self.data.splines[si].beziers, self.data.splines[si].lengths, self.data.splines[si].total_length, ) = get_real_beziers_and_lengths(self.data.splines[si], resolution_factor) - print(f"total length {self.data.splines[si].total_length}") def get_real_beziers_and_lengths(bezier_spline_obj, resolution_factor): @@ -391,7 +405,7 @@ def calc_point_on_bezier_spline( 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) - if not isinstance(bezier_spline_obj, BezierSpline) + if not isinstance(bezier_spline_obj, HookBezierSpline) else ( bezier_spline_obj.beziers, bezier_spline_obj.lengths, @@ -411,7 +425,7 @@ def calc_point_on_bezier_spline( beziers, lengths, total_length = ( get_real_beziers_and_lengths(bezier_spline_obj, resolution_factor) - if not isinstance(bezier_spline_obj, BezierSpline) + if not isinstance(bezier_spline_obj, HookBezierSpline) else ( bezier_spline_obj.beziers, bezier_spline_obj.lengths, @@ -460,16 +474,15 @@ def calc_point_on_bezier_curve( output_spline_index=False, resolution_factor=1.0, ): - # bezier_curve = BezierCurve(bezier_curve_obj) - # curve = bezier_curve.data - curve = bezier_curve_obj.data + bezier_curve = HookBezierCurve(bezier_curve_obj) + curve = bezier_curve.data + # curve = bezier_curve_obj.data # Loop through all splines in the curve total_length = 0 for i, spline in enumerate(curve.splines): resolution = int(spline.resolution_u * resolution_factor) length = spline.calc_length(resolution=resolution) - print(f"{utils.LINE()} {length=}") if total_length + length > distance or i == len(curve.splines) - 1: if output_spline_index and output_tangent: # return value from c_p_o_b_s is a tuple From a5602a6095a583720fbb06e5c0d55ad285956bae Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Thu, 5 Jun 2025 13:20:39 +0200 Subject: [PATCH 103/103] bump version number to v0.0.12 --- README.md | 2 +- __init__.py | 2 +- common/utils.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ee15c47..c4fa035 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ / ___ \| |_) | |___ ___) | |_| | /_/ \_\____/ \____|____/|____/ ``` -v0.0.11 +v0.0.12 Convenience addon to work with 3D typography in Blender and Cinema4D. diff --git a/__init__.py b/__init__.py index 697e857..c4fc090 100644 --- a/__init__.py +++ b/__init__.py @@ -16,7 +16,7 @@ from .common import Font, utils bl_info = { "name": "ABC3D", "author": "Jakob Schlötter, Studio Pointer*", - "version": (0, 0, 11), + "version": (0, 0, 12), "blender": (4, 1, 0), "location": "VIEW3D", "description": "Convenience addon for 3D fonts", diff --git a/common/utils.py b/common/utils.py index 4a036f8..7798a3c 100644 --- a/common/utils.py +++ b/common/utils.py @@ -8,7 +8,7 @@ def get_version_minor(): def get_version_patch(): - return 11 + return 12 def get_version_string():