import importlib import os import queue import re import bpy import bpy_types import mathutils # import time # for debugging performance # then import dependencies for our addon if "Font" in locals(): importlib.reload(Font) else: from .common import Font if "utils" in locals(): importlib.reload(utils) else: from .common import utils execution_queue = queue.Queue() lock_depsgraph_update_n_times = -1 # 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"): obj.data.transform(mb) for c in obj.children: c.matrix_local = mb @ c.matrix_local obj.matrix_basis.identity() # 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}" # Ensure it's a curve object # TODO: no raising, please def get_curve_length(curve_obj, resolution=-1): total_length = 0 curve = curve_obj.data # 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): 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 ): curve_line_lengths = get_curve_line_lengths(curve_obj, resolution) total_length = 0 for cll in curve_line_lengths: total_length += cll if current_advance - previous_glyph_advance < total_length: 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 if p1 == h1 and p2 == h2: return p1 + t * (p2 - p1) 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 # 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 def calc_tangent_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 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 + (-3 * (t**2) + 6 * t * (1 - t)) * h2 + (3 * t**2) * p2 ).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( 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() input_rotation = mathutils.Euler(input_rotations[i]) if vector.length < 1e-6: output_rotations[i] = input_rotation.to_matrix() continue old_rotation = input_rotation.to_matrix() old_axis = (old_rotation @ local_main_axis).normalized() new_axis = vector # rotation_axis = (-(old_axis) + new_axis).normalized() rotation_axis = old_axis.cross(new_axis).normalized() 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() # full_angle = radians(sqrt((4 * pow(input_rotation.to_quaternion().dot(mathutils.Quaternion(vectors[i].normalized())), 2) - 3))) # dot = old_axis.dot(new_axis) # normalized_diff = (old_axis - new_axis).normalized() # full_angle = acos(min((old_axis * new_axis + normalized_diff.dot(2)).length, 1)) full_angle = old_axis.angle(new_axis) angle = factors[i] * full_angle rotation = mathutils.Quaternion(rotation_axis, angle).to_matrix() new_rotation_matrix = old_rotation @ rotation output_rotations[i] = new_rotation_matrix return [mat.to_4x4() for mat in output_rotations] def calc_bezier_length(bezier_point_1, bezier_point_2, resolution=20): step = 1 / resolution previous_p = bezier_point_1.co length = 0 for i in range(-1, resolution): t = (i + 1) * step p = calc_point_on_bezier(bezier_point_1, bezier_point_2, t) length += (p - previous_p).length previous_p = p 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 = [] 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 ): # 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( 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)) else: 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)) return travel, tangent else: return travel 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) 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 location = p.co + travel if output_tangent: p2 = bezier_spline_obj.bezier_points[1] tangent = calc_tangent_on_bezier(p, p2, 0) return location, tangent else: return location 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 for i in range(0, len(beziers)): if iterated_distance + lengths[i] > 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) if output_tangent: tangent = calc_tangent_on_bezier(beziers[i][0], beziers[i][1], d) return location, tangent else: return location iterated_distance += lengths[i] # 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) 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, ): # bezier_curve = BezierCurve(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 # so we need to append tuple + tuple 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, ) else: return calc_point_on_bezier_spline( spline, (distance - total_length), output_tangent, resolution_factor ) total_length += length # 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)] def find_objects_by_name(objects, equals="", contains="", startswith="", endswith=""): # handle equals if equals != "": index = objects.find(equals) if index >= 0: 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 ] 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 ] # 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): fonts = find_objects_by_custom_property(fontcollection.objects, "is_font", True) for font in fonts: if font["font_name"] == font_name and font.parent is 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) 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"]) 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) # ensure custom properties are set font_obj["font_name"] = obj["font_name"] 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"]) 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 fontcollection.objects.link(face_obj) # ensure custom properties are set face_obj["face_name"] = obj["face_name"] face_obj["font_name"] = obj["font_name"] if face_obj.parent != font_obj: face_obj.parent = font_obj # create glyphs if it does not exist 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" fontcollection.objects.link(glyphs_obj) glyphs_obj.parent = face_obj elif len(glyphs_objs) > 1: 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] glyphs_obj["face_name"] = obj["face_name"] glyphs_obj["font_name"] = obj["font_name"] def get_hash(o): 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"] ): if get_hash(other_obj) == get_hash(obj) and not allow_duplicates: return other_obj # and now parent it! if obj.parent != glyphs_obj: obj.parent = glyphs_obj for c in obj.users_collection: c.objects.unlink(obj) if fontcollection.objects.find(obj.name) < 0: fontcollection.objects.link(obj) 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"] face_name = a["face_name"] glyph = a["glyph"] if font_name not in fonts: 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: for face_name in fonts[font_name]: 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( "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, ) fontcollection = bpy.data.collections.get("ABC3D") if fontcollection is None: fontcollection = bpy.data.collections.new("ABC3D") modified_font_faces = [] all_glyph_os = [] for o in bpy.context.scene.objects: if marker_property in o: if "type" in o and o["type"] == "glyph": all_glyph_os.append(o) for o in all_glyph_os: glyph_id = o["glyph"] font_name = o["font_name"] face_name = o["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] 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}) for mff in modified_font_faces: mff_glyphs = [] 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"{utils.prefix()}:: modified font face {mff=} could not be accessed." ) continue # iterate glyphs for g in face.glyphs: # iterate alternates for glyph in face.glyphs[g]: 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(mff_glyphs[0]) if h != 0: face.unit_factor = 1 / h update_available_fonts() remove_list = [] for o in bpy.context.scene.collection.all_objects: if o.name not in fontcollection.all_objects: if marker_property in o and o[marker_property] == True: remove_list.append(o) simply_delete_objects(remove_list) # 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_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") if fontcollection is None: print( f"{utils.prefix()}::clean_fontcollection: failed because 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 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"{utils.prefix()}::update_available_fonts: {__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 "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 def getPreferences(context): preferences = context.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") if os.path.exists(font_dir): for file in os.listdir(font_dir): if file.endswith(".glb") or file.endswith(".gltf"): font_path = os.path.join(font_dir, file) # 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}") 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") if os.path.exists(font_dir): for file in os.listdir(font_dir): if file.endswith(".glb") or file.endswith(".gltf"): font_path = os.path.join(font_dir, file) # 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}") register_font_from_filepath(font_path) message_memory = [] def ShowMessageBox(title="Message Box", icon="INFO", message="", prevent_repeat=False): """Show a simple message box taken from `Link here `_ :param title: The title shown in the message top bar :type title: str :param icon: The icon to be shown in the message top bar :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") butils.ShowMessageBox(message=myLines) or: .. code-block:: python 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: return message_memory.append([title, icon, 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) 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): if recursive: try: if hasattr(g, "children") and len(g.children) > 0: completely_delete_objects(g.children) except ReferenceError: # not important pass try: bpy.data.objects.remove(g, do_unlink=True) except ReferenceError: # not important pass except RuntimeError: 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) is not None or re.match(".*_metrics.[\d]{3}$", o.name) is not None ) and is_mesh(o) def is_text_object(o): if f"{utils.prefix()}_type" in o: 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_object(o): if f"{utils.prefix()}_type" in o: return o[f"{utils.prefix()}_type"] == "glyph" try: return ( o.parent is not None and "glyphs" in o.parent.name and is_mesh(o) and not is_metrics_object(o) ) except ReferenceError: return False def is_glyph(o): return is_glyph_object(o) def update_types(): scene = bpy.context.scene abc3d_data = scene.abc3d_data for t in abc3d_data.available_texts: t.text_object[f"{utils.prefix()}_type"] = "textobject" for g in t.glyphs: g.glyph_object[f"{utils.prefix()}_type"] = "glyph" # blender bound_box vertices # # 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_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: 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): 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(availability.missing) > 0 and allow_replacement: replacement_search = "" for m in availability.missing: if m.isalpha(): replacement_search += m.swapcase() r = Font.test_availability(font_name, face_name, replacement_search) loadable += r.unloaded # not update (loaded, missing, files), we only use loadable/maybe later if len(loadable) > 0: for filepath in availability.filepaths: 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: 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 def is_bezier(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": return False return True 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: 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] ): return COMPARE_TEXT_OBJECT_DIFFER # else same else: return COMPARE_TEXT_OBJECT_DIFFER # else same 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 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 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, 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 ): 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": text_object[object_key] = text_properties["text_id"] else: 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 != "" ): 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 = [] 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")]) 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 else: t_text = predict_actual_text(text_properties) if t_text == text: is_good_text = True if is_good_text: 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 inner_node = None for c in glyph_object.children: if c.name.startswith(f"{glyph_id}_mesh"): inner_node = c if inner_node is None: 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 if not found_reconstructable_glyphs: text_properties.actual_text = "" 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: font_name = text_properties["font_name"] face_name = text_properties["face_name"] text_properties.font = f"{font_name} {face_name}" 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 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) def test_finding(): scene = bpy.context.scene abc3d_data = scene.abc3d_data text_id = find_free_text_id() t = abc3d_data.available_texts.add() t["text_id"] = text_id o = bpy.context.active_object 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 # 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 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")] 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) if len(text_properties.glyphs) == 0: 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 g.glyph_object is None: return True elif g.glyph_object.parent is 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 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 oo = o for i in range(0, max_depth): oo = oo.parent if oo == parent: return True if oo is None: return False return False 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() 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 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 ): """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 """ # 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() 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: return False distribution_type = "CALCULATE" if is_bezier(mom) else "FOLLOW_PATH" predicted_text = predict_actual_text(text_properties) ensure_glyphs(text_properties, predicted_text) curve_length = get_curve_length(mom) advance = text_properties.offset glyph_advance = 0 glyph_index = 0 is_command = False 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 if c == "\\": is_command = True continue is_newline = False if is_command: if c == "n": is_newline = True next_line_advance = get_next_line_advance(mom, advance, glyph_advance) if advance == next_line_advance: 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 spline_index = 0 ############### HANDLE SPACES 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 ############### GLYPH PROPERTIES glyph_properties = text_properties.glyphs[glyph_index] # ensure_glyph_object(text_properties, glyph_properties) ############### ACTUAL TEXT actual_text += glyph_id ############### NODE SCENE MANAGEMENT # 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) 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 = ( 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" 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" # 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 ) # 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) 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: # glyph_properties.glyph_object.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) if previous_glyph_object_rotation_mode: glyph_properties.glyph_object.rotation_mode = ( previous_glyph_object_rotation_mode ) # outer_node.hide_viewport = True ############### PREPARE FOR THE NEXT glyph_advance = ( 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 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 ) if psi == si: n_max = 100 n = 0 while ( (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, 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 and n < n_max ): curve_compensation = curve_compensation + glyph_advance * 0.01 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 previous_spline_index = spline_index text_properties["actual_text"] = actual_text return True verification_object = { f"{utils.prefix()}_type": "textobject", 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, 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 # blender bound_box vertices # # 3------7. # |`. | `. +y # | `2------6 -z | # | | | | `. | # 0---|--4. | `+--- +x # `. | `.| # `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 obj = bpy.data.objects.new(mesh.name, mesh) obj["font_name"] = glyph["font_name"] obj["face_name"] = glyph["face_name"] obj["glyph"] = glyph["glyph"] obj[f"{utils.prefix()}_type"] = "metrics" # remove already existing metrics remove_metrics = [] for c in glyph.children: if is_metrics_object(c): remove_metrics.append(c) if len(remove_metrics) > 0: completely_delete_objects(remove_metrics) col = glyph.users_collection[0] col.objects.link(obj) # bpy.context.view_layer.objects.active = obj obj.parent = glyph 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], ] 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], ] 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], ] mesh.from_pydata(verts, edges, faces) old_mesh = obj.data 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 = [] mesh.from_pydata(verts, edges, faces) old_mesh = obj.data 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) 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]) 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_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. # |`. | `. +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)] 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 # generates a metrics bounding box # where x-width comes from bb # and y-height + z-depth from bb_uebermetrics # # @param bb # @param bb_uebermetrics # # @return metrics def get_metrics_bound_box(bb, bb_uebermetrics): metrics = [[0] * 3] * 8 # hurrays if type(bb_uebermetrics) == bpy.types.bpy_prop_array: metrics = bound_box_as_array(bb_uebermetrics) else: metrics = bb_uebermetrics.copy() metrics[0][0] = bb[0][0] metrics[1][0] = bb[1][0] metrics[2][0] = bb[2][0] metrics[3][0] = bb[3][0] metrics[4][0] = bb[4][0] metrics[5][0] = bb[5][0] metrics[6][0] = bb[6][0] metrics[7][0] = bb[7][0] return metrics def get_metrics_object(o): if is_glyph(o): for c in o.children: if is_metrics_object(c): 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 targets = [] reference_bound_box = None for o in objects: is_possibly_glyph = is_glyph(o) if is_possibly_glyph: metrics = [] for c in o.children: if is_metrics_object(c): metrics.append(c) if len(metrics) == 0: targets.append(o) 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 ) else: for m in metrics: 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 metrics = [] for o in objects: for c in o.children: if is_metrics_object(c): 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 if len(objects) == 0: return "no objects selected" # define the reference_bound_box reference_bound_box = None if type(bpy.context.active_object) == type(None): return "no active_object, but align_to_active_object is True" for c in bpy.context.active_object.children: if is_metrics_object(c): reference_bound_box = bound_box_as_array(c.bound_box) break if type(reference_bound_box) == type(None): if not is_mesh(bpy.context.active_object): return "active_object is not a mesh and does not have a metrics child" reference_bound_box = bound_box_as_array(bpy.context.active_object.bound_box) # do it for o in objects: is_possibly_glyph = is_glyph(o) if is_possibly_glyph and o is not bpy.context.active_object: metrics = [] for c in o.children: if is_metrics_object(c): metrics.append(c) bb = None if len(metrics) == 0: bb = get_metrics_bound_box(o.bound_box, reference_bound_box) else: 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 if len(objects) == 0: return "no objects selected" targets = [] reference_bound_box = None for o in objects: is_possibly_glyph = is_glyph(o) if is_possibly_glyph: metrics = [] for c in o.children: if is_metrics_object(c): metrics.append(c) if len(metrics) == 0: 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 ) targets.append(o) for t in targets: metrics = [] for c in t.children: if is_metrics_object(c): metrics.append(c) bound_box = None if len(metrics) == 0: 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) completely_delete_objects(metrics) 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 ""