From 20fb69465bb37ac7b070710ed52247ae61b88e91 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sat, 7 Dec 2024 14:57:33 +0100 Subject: [PATCH] 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