diff --git a/README.md b/README.md index c4fa035..ee15c47 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ / ___ \| |_) | |___ ___) | |_| | /_/ \_\____/ \____|____/|____/ ``` -v0.0.12 +v0.0.11 Convenience addon to work with 3D typography in Blender and Cinema4D. diff --git a/__init__.py b/__init__.py index c4fc090..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, 12), + "version": (0, 0, 11), "blender": (4, 1, 0), "location": "VIEW3D", "description": "Convenience addon for 3D fonts", @@ -143,40 +143,13 @@ 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=0, # also change in alternate_get_callback - get=alternate_get_callback, - set=alternate_set_callback, + default=-1, update=update_callback, ) glyph_object: bpy.props.PointerProperty(type=bpy.types.Object) @@ -294,7 +267,6 @@ 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() @@ -688,34 +660,42 @@ 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_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 + 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 return None - def get_active_glyph_properties(self, text_properties): - if text_properties is None: - return None + def get_active_glyph_properties(self): a_o = bpy.context.active_object if a_o is not None: - if 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"] - if len(text_properties.glyphs) <= glyph_index: - return None - return text_properties.glyphs[glyph_index] + return bpy.context.scene.abc3d_data.available_texts[text_index].glyphs[ + glyph_index + ] else: - for g in text_properties.glyphs: - if butils.is_or_has_parent(a_o, g.glyph_object, max_depth=4): - return g + 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 @classmethod @@ -729,7 +709,7 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): layout = self.layout props = self.get_active_text_properties() - glyph_props = self.get_active_glyph_properties(props) + glyph_props = self.get_active_glyph_properties() if props is None or props.text_object is None: # this should not happen @@ -741,22 +721,6 @@ 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") @@ -773,26 +737,8 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): if glyph_props is None: return box = layout.box() - box.label(text=f"selected character: {glyph_props.glyph_id}") + box.label(text=f"{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): @@ -1648,10 +1594,9 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): def do_autodetect_names(self, name: str): ifxsplit = name.split("_") if len(ifxsplit) < 4: - print( - f"{utils.prefix()}::CreateFontFromObjects: name could not be autodetected {name}" - ) - print(f"{utils.prefix()}::CreateFontFromObjects: split: {ifxsplit=}") + print(f"name could not be autodetected {name}") + print("split:") + print(ifxsplit) return self.font_name, self.face_name detected_font_name = f"{ifxsplit[1]}_{ifxsplit[2]}" detected_face_name = ifxsplit[3] @@ -1726,11 +1671,9 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): row.label(text=f"{k} → {Font.known_misspellings[k]}{character}") def execute(self, context): - print(f"{utils.prefix()}::CreateFontFromObjects: executing {self.bl_idname}") + print(f"executing {self.bl_idname}") if len(context.selected_objects) == 0: - print( - f"{utils.prefix()}::CreateFontFromObjects: cancelled {self.bl_idname} - no objects selected" - ) + print(f"cancelled {self.bl_idname} - no objects selected") return {"CANCELLED"} global shared scene = bpy.context.scene @@ -1747,7 +1690,7 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): currentObjects = [] for o in context.selected_objects: if o.name not in currentObjects: - print(f"{utils.prefix()}::CreateFontFromObjects: processing {o.name}") + print(f"processing {o.name}") process_object = True if self.autodetect_names: font_name, face_name = self.do_autodetect_names(o.name) @@ -1784,9 +1727,7 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): f.face_name = face_name else: - print( - f"{utils.prefix()}::CreateFontFromObjects: import warning: 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"} @@ -2036,10 +1977,6 @@ 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 diff --git a/butils.py b/butils.py index 30b0a47..28dbd0f 100644 --- a/butils.py +++ b/butils.py @@ -217,132 +217,6 @@ 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 HookBezierSplinePoint: - def __init__( - self, - handle_left: mathutils.Vector, - co: mathutils.Vector, - handle_right: mathutils.Vector, - ): - 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 HookBezierSpline: - def __init__( - self, - n: int, - use_cyclic_u: bool, - resolution_u: int, - ): - self.bezier_points = [HookBezierSplinePoint] * 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 - return self.total_length - - -class HookBezierData: - def __init__(self, n): - self.splines = [HookBezierSpline] * n - - -class HookBezierCurve: - def __init__(self, blender_curve: bpy.types.Object, resolution_factor=1.0): - self.data = HookBezierData(len(blender_curve.data.splines)) - i = 0 - hooks = get_hook_modifiers(blender_curve) - for si, blender_spline in enumerate(blender_curve.data.splines): - 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] = HookBezierSplinePoint( - blender_bezier_point.handle_left, - blender_bezier_point.co, - blender_bezier_point.handle_right, - ) - 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 - ) - 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_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) - - def get_real_beziers_and_lengths(bezier_spline_obj, resolution_factor): beziers = [] lengths = [] @@ -403,14 +277,8 @@ 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) - if not isinstance(bezier_spline_obj, HookBezierSpline) - else ( - bezier_spline_obj.beziers, - bezier_spline_obj.lengths, - bezier_spline_obj.total_length, - ) + 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 @@ -423,14 +291,8 @@ def calc_point_on_bezier_spline( else: return location - beziers, lengths, total_length = ( - get_real_beziers_and_lengths(bezier_spline_obj, resolution_factor) - if not isinstance(bezier_spline_obj, HookBezierSpline) - else ( - bezier_spline_obj.beziers, - bezier_spline_obj.lengths, - bezier_spline_obj.total_length, - ) + beziers, lengths, total_length = get_real_beziers_and_lengths( + bezier_spline_obj, resolution_factor ) iterated_distance = 0 @@ -474,9 +336,7 @@ def calc_point_on_bezier_curve( output_spline_index=False, resolution_factor=1.0, ): - bezier_curve = HookBezierCurve(bezier_curve_obj) - curve = bezier_curve.data - # curve = bezier_curve_obj.data + curve = bezier_curve_obj.data # Loop through all splines in the curve total_length = 0 @@ -803,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 because fontcollection is none" + f"{utils.prefix()}::clean_fontcollection: failed beacause fontcollection is none" ) return False @@ -1151,11 +1011,9 @@ def predict_actual_text(text_properties): ) t_text = text_properties.text for c in availability.missing: - C = c.swapcase() - if C in AVAILABILITY.missing: - t_text = t_text.replace(c, "") - else: - t_text = t_text.replace(c, C) + t_text = t_text.replace(c, "") + for c in AVAILABILITY.missing: + t_text = t_text.replace(c, "") return t_text @@ -1326,7 +1184,6 @@ 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 @@ -1442,7 +1299,8 @@ 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_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"): @@ -1451,9 +1309,6 @@ 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 @@ -1564,21 +1419,10 @@ 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 get_text_difference_index(text_properties.actual_text, predicted_text) + return True if len(text_properties.glyphs) == 0: return True @@ -1630,7 +1474,6 @@ 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() @@ -1646,187 +1489,6 @@ 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") - transfer_text_properties_to_text_object( - text_properties, text_properties.text_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 ): @@ -1847,11 +1509,7 @@ 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: @@ -1859,8 +1517,27 @@ def set_text_on_curve( distribution_type = "CALCULATE" if is_bezier(mom) else "FOLLOW_PATH" - predicted_text = predict_actual_text(text_properties) - ensure_glyphs(text_properties, predicted_text) + # 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) curve_length = get_curve_length(mom) advance = text_properties.offset @@ -1870,9 +1547,6 @@ 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 @@ -1896,31 +1570,90 @@ def set_text_on_curve( spline_index = 0 - ############### HANDLE SPACES + ############### GET GLYPH - if glyph_id not in predicted_text: + 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: advance = advance + space_width * text_properties.font_size - continue + 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 ############### GLYPH PROPERTIES - glyph_properties = text_properties.glyphs[glyph_index] - # ensure_glyph_object(text_properties, glyph_properties) + glyph_properties = ( + text_properties.glyphs[glyph_index] + if not regenerate + else text_properties.glyphs.add() + ) - ############### ACTUAL TEXT - - actual_text += glyph_id + 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 ############### NODE SCENE MANAGEMENT - # outsourced to ensure_glyph_object + 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 ############### 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) @@ -1948,29 +1681,26 @@ def set_text_on_curve( outer_node.constraints["Follow Path"].up_axis = "UP_Y" spline_index = 0 elif distribution_type == "CALCULATE": - 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" + 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 # 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_hooked, applied_advance, True, True - # ) # check if we are on a new line if spline_index != previous_spline_index: is_newline = True # position - glyph_properties.glyph_object.location = ( - location + text_properties.translation - ) + outer_node.location = location + text_properties.translation # orientation / rotation mask = [0] @@ -1988,21 +1718,21 @@ def set_text_on_curve( q = mathutils.Quaternion() q.rotate(text_properties.orientation) - glyph_properties.glyph_object.rotation_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: - # glyph_properties.glyph_object.rotation_quaternion = (mom.matrix_world.inverted().to_3x3() @ motor[0].to_3x3() @ q.to_matrix()).to_quaternion() + # outer_node.rotation_quaternion = (mom.matrix_world.inverted().to_3x3() @ motor[0].to_3x3() @ q.to_matrix()).to_quaternion() # # scale - glyph_properties.glyph_object.scale = (scalor, scalor, scalor) + outer_node.scale = (scalor, scalor, scalor) - if previous_glyph_object_rotation_mode: - glyph_properties.glyph_object.rotation_mode = ( - previous_glyph_object_rotation_mode - ) + 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 @@ -2073,7 +1803,8 @@ def set_text_on_curve( glyph_index += 1 previous_spline_index = spline_index - text_properties["actual_text"] = actual_text + if regenerate: + text_properties["actual_text"] = actual_text return True diff --git a/common/Font.py b/common/Font.py index aebf702..d68518c 100644 --- a/common/Font.py +++ b/common/Font.py @@ -266,13 +266,7 @@ def get_glyphs(font_name, face_name, glyph_id): return glyphs_for_id -def get_glyph( - font_name: str, - face_name: str, - glyph_id: str, - alternate: int = 0, - alternate_tolerant: bool = True, -): +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 @@ -282,10 +276,6 @@ def get_glyph( :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` @@ -293,21 +283,12 @@ def get_glyph( glyphs = get_glyphs(font_name, face_name, glyph_id) - if len(glyphs) == 0: + 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 - 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] diff --git a/common/utils.py b/common/utils.py index 7798a3c..4a036f8 100644 --- a/common/utils.py +++ b/common/utils.py @@ -8,7 +8,7 @@ def get_version_minor(): def get_version_patch(): - return 12 + return 11 def get_version_string():