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 9ed5a48..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", @@ -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() @@ -660,42 +688,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 +729,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 @@ -721,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") @@ -737,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): @@ -1594,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] @@ -1671,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 @@ -1690,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) @@ -1727,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"} @@ -1977,6 +2036,10 @@ 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 28dbd0f..30b0a47 100644 --- a/butils.py +++ b/butils.py @@ -217,6 +217,132 @@ 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 = [] @@ -277,8 +403,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, HookBezierSpline) + 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 +423,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, HookBezierSpline) + else ( + bezier_spline_obj.beziers, + bezier_spline_obj.lengths, + bezier_spline_obj.total_length, + ) ) iterated_distance = 0 @@ -336,7 +474,9 @@ def calc_point_on_bezier_curve( output_spline_index=False, resolution_factor=1.0, ): - 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 @@ -663,7 +803,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 +1151,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 +1326,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,8 +1442,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["glyph_object"] = glyph_object inner_node = None for c in glyph_object.children: if c.name.startswith(f"{glyph_id}_mesh"): @@ -1309,6 +1451,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 @@ -1419,10 +1564,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 @@ -1474,6 +1630,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() @@ -1489,6 +1646,187 @@ 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 ): @@ -1509,7 +1847,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: @@ -1517,27 +1859,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 @@ -1547,6 +1870,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 @@ -1570,90 +1896,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) @@ -1681,26 +1948,29 @@ 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( 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 - outer_node.location = location + text_properties.translation + glyph_properties.glyph_object.location = ( + location + text_properties.translation + ) # orientation / rotation mask = [0] @@ -1718,21 +1988,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 @@ -1803,8 +2073,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] 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():