import bpy import mathutils import queue import importlib import os import re from multiprocessing import Process # 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() # 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() 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 # 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 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 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() from math import radians, sqrt, pi, acos 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 calc_point_on_bezier_spline(bezier_spline_obj, distance, output_tangent = False, resolution_factor = 1.0): # what's the point of just one point # assert len(bezier_spline_obj.bezier_points) >= 2 # however, maybe let's have it not crash and do this if len(bezier_spline_obj.bezier_points) < 1: print("butils::calc_point_on_bezier_spline: whoops, no points. panicking. return 0,0,0") 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 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 = 0 n_bezier_points = len(bezier_spline_obj.bezier_points) for i in range(0, len(bezier_spline_obj.bezier_points) - 1): bezier = [ bezier_spline_obj.bezier_points[i], bezier_spline_obj.bezier_points[i + 1] ] length = calc_bezier_length(bezier[0], bezier[1], int(bezier_spline_obj.resolution_u * resolution_factor)) total_length += length beziers.append(bezier) lengths.append(length) # if total_length > distance: # break 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) 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, resolution_factor = 1.0): 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) if total_length + length > distance or i == len(curve.splines) - 1: return calc_point_on_bezier_spline(spline, (distance - total_length), output_tangent, resolution_factor) total_length += length # TODO: can this fail? # def get_objects_by_name(name, startswith="", endswith=""): # return [obj for obj in bpy.context.scene.objects if obj.name.startswith(startswith) and if obj.name.endswith(endswith)] 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] 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 == 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 == 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 == 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"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 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 not font_name in fonts: fonts[font_name] = {} if not face_name 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(f"{bl_info['name']} 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 = [] remove_list = [] all_objects = [] 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) else: remove_list.append(o) for o in all_glyph_os: glyph_id = o["glyph"] font_name = o["font_name"] face_name = o["face_name"] del o[marker_property] glyph_obj = move_in_fontcollection( o, fontcollection) Font.add_glyph( font_name, face_name, glyph_id, glyph_obj) 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}) if glyph_obj != o: remove_list.append(o) for mff in modified_font_faces: glyphs = [] face = Font.fonts[mff["font_name"]].faces[mff["face_name"]] # iterate glyphs for g in face.glyphs: # iterate alternates for glyph in face.glyphs[g]: glyphs.append(glyph) if len(glyphs) > 0: add_default_metrics_to_objects(glyphs) # calculate unit factor h = get_glyph_height(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 not o.name 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 update_available_fonts(): abc3d_data = bpy.context.scene.abc3d_data for font_name in Font.fonts.keys(): for face_name in Font.fonts[font_name].faces.keys(): found = False for f in abc3d_data.available_fonts.values(): if font_name == f.font_name and face_name == f.face_name: found = True if not found: f = abc3d_data.available_fonts.add() f.font_name = font_name f.face_name = face_name print(f"{__name__} added {font_name} {face_name}") # def update_available_texts(): # abc3d_data = bpy.context.scene.abc3d_data # for o in bpy.context.scene.objects: # if "linked_textobject" in o.keys(): # i = o["linked_textobject"] # found = False # if len(abc3d_data.available_texts) > i: # if abc3d_data.available_texts[i].glyphs 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 = f"{preferences.assets_dir}/fonts" 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 = f"{preferences.assets_dir}/fonts" 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) def ShowMessageBox(title = "Message Box", icon = 'INFO', message=""): """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=) """ 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): context_override = bpy.context.copy() context_override["selected_objects"] = list(objs) with bpy.context.temp_override(**context_override): bpy.ops.object.delete() def completely_delete_objects(objs): simply_delete_objects(objs) # remove deleted objects # this is necessary for g in objs: if type(g) != type(None): try: bpy.data.objects.remove(g, do_unlink=True) except ReferenceError as e: # not important pass def is_mesh(o): return type(o.data) == bpy.types.Mesh def is_metrics_object(o): return (re.match(".*_metrics$", o.name) != None or re.match(".*_metrics.[\d]{3}$", o.name) != None) and is_mesh(o) def is_glyph(o): try: return type(o.parent) is not type(None) \ and "glyphs" in o.parent.name \ and is_mesh(o) \ and not is_metrics_object(o) except ReferenceError as e: return False # 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_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): loaded, missing, loadable, files = Font.test_glyphs_availability( font_name, face_name, text) if len(loadable) > 0: for filepath in files: load_font_from_filepath(filepath, loadable, font_name, face_name) return True def set_text_on_curve(text_properties, recursive=True): # starttime = time.perf_counter_ns() mom = text_properties.text_object if mom.type != "CURVE": return False regenerate = False glyph_objects = [] for i, g in enumerate(text_properties.glyphs): glyph_objects.append(g.glyph_object) # check if perhaps one glyph was deleted if (type(g.glyph_object) == type(None) or type(g.glyph_object.parent) == type(None) or g.glyph_object.parent.users_collection != g.glyph_object.users_collection): regenerate = True elif len(text_properties.text) > i and g.glyph_id != text_properties.text[i]: regenerate = True elif len(text_properties.text) > i and (g.glyph_object[f"{utils.prefix()}_font_name"] != text_properties.font_name or g.glyph_object[f"{utils.prefix()}_face_name"] != text_properties.face_name): regenerate = True if len(text_properties.text) != len(text_properties.glyphs): regenerate = True # if we regenerate.... delete objects if regenerate: # loaded, missing, maybe, files = Font.test_glyphs_availability( # text_properties.font_name, # text_properties.face_name, # text_properties.text) # if len(maybe) > 0 and recursive: # print(f"doing the thing {len(files)} times") # for filepath in files: # def loader(): # set_text_on_curve(text_properties, False) # print(f"loading font from filepath {filepath} {maybe}") # load_font_from_filepath(filepath, maybe) # print(f"font: {text_properties.font_name} face: {text_properties.face_name}") # print("text",text_properties.text) # text_properties.font_size = text_properties.font_size # # run_in_main_thread(loader) # return completely_delete_objects(glyph_objects) # context_override = bpy.context.copy() # context_override["selected_objects"] = list(glyph_objects) # with bpy.context.temp_override(**context_override): # bpy.ops.object.delete() # # remove deleted objects # # this is necessary # for g in glyph_objects: # if type(g) != type(None): # bpy.data.objects.remove(g, do_unlink=True) text_properties.glyphs.clear() #TODO: fix selection with context_override previous_selection = bpy.context.selected_objects bpy.ops.object.select_all(action='DESELECT') selected_objects = [] curve_length = get_curve_length(mom) advance = 0 glyph_advance = 0 is_command = False for i, c in enumerate(text_properties.text): if c == '\\': is_command = True continue if is_command: if c == 'n': next_line_advance = get_next_line_advance(mom, advance, glyph_advance) if advance == next_line_advance: # self.report({'INFO'}, f"would like to add new line for {text_properties.text} please") print(f"would like to add new line for {text_properties.text} please") # TODO: add a new line advance = next_line_advance continue is_command = False glyph_id = c glyph = Font.get_glyph(text_properties.font_name, text_properties.face_name, glyph_id) if glyph == None: # self.report({'ERROR'}, f"Glyph not found for {font_name} {face_name} {glyph_id}") print(f"Glyph not found for {text_properties.font_name} {text_properties.face_name} {glyph_id}") continue ob = None if regenerate: ob = bpy.data.objects.new(f"{glyph_id}", glyph.data) ob[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id ob[f"{utils.prefix()}_font_name"] = text_properties.font_name ob[f"{utils.prefix()}_face_name"] = text_properties.face_name else: ob = text_properties.glyphs[i].glyph_object distribution_type = 'CALCULATE' if distribution_type == 'FOLLOW_PATH': ob.constraints.new(type='FOLLOW_PATH') ob.constraints["Follow Path"].target = mom ob.constraints["Follow Path"].use_fixed_location = True ob.constraints["Follow Path"].offset_factor = advance / curve_length ob.constraints["Follow Path"].use_curve_follow = True ob.constraints["Follow Path"].forward_axis = "FORWARD_X" ob.constraints["Follow Path"].up_axis = "UP_Y" elif distribution_type == 'CALCULATE': location, tangent = calc_point_on_bezier_curve(mom, advance, True) ob.location = mom.matrix_world @ (location + text_properties.translation) if not text_properties.ignore_orientation: mask = [0] input_rotations = [mathutils.Vector((0.0, 0.0, 0.0))] vectors = [tangent] factors = [1.0] local_main_axis = mathutils.Vector((1.0, 0.0, 0.0)) motor = align_rotations_auto_pivot(mask, input_rotations, vectors, factors, local_main_axis) if ob.rotation_mode != 'QUATERNION': ob.rotation_mode = 'QUATERNION' q = mathutils.Quaternion() q.rotate(text_properties.orientation) ob.rotation_quaternion = (mom.matrix_world @ motor[0] @ q.to_matrix().to_4x4()).to_quaternion() else: q = mathutils.Quaternion() q.rotate(text_properties.orientation) ob.rotation_quaternion = q # ob.rotation_quaternion = (mom.matrix_world @ q.to_matrix().to_4x4()).to_quaternion() face = Font.fonts[text_properties.font_name].faces[text_properties.face_name] scalor = face.unit_factor * text_properties.font_size glyph_advance = get_glyph_advance(glyph) * scalor + text_properties.letter_spacing # now we need to compensate for curvature # otherwise letters will be closer together the curvier the bezier is # this could be done more efficiently, but whatever curve_compensation = 0 if text_properties.compensate_curvature and glyph_advance > 0: previous_location = calc_point_on_bezier_curve(mom, advance, False) new_location = calc_point_on_bezier_curve(mom, advance + glyph_advance, False) while (previous_location - new_location).length > glyph_advance: curve_compensation = curve_compensation - glyph_advance * 0.01 new_location = calc_point_on_bezier_curve(mom, advance + glyph_advance + curve_compensation, False) while (previous_location - new_location).length < glyph_advance: curve_compensation = curve_compensation + glyph_advance * 0.01 new_location = calc_point_on_bezier_curve(mom, advance + glyph_advance + curve_compensation, False) ob.scale = (scalor, scalor, scalor) advance = advance + glyph_advance + curve_compensation if regenerate: mom.users_collection[0].objects.link(ob) glyph_data = text_properties.glyphs.add() glyph_data.glyph_id = glyph_id glyph_data.glyph_object = ob glyph_data.letter_spacing = 0 ob.select_set(True) if regenerate: mom.select_set(True) mom[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id mom[f"{utils.prefix()}_font_name"] = text_properties.font_name mom[f"{utils.prefix()}_face_name"] = text_properties.face_name mom[f"{utils.prefix()}_font_size"] = text_properties.font_size mom[f"{utils.prefix()}_letter_spacing"] = text_properties.letter_spacing mom[f"{utils.prefix()}_orientation"] = text_properties.orientation mom[f"{utils.prefix()}_translation"] = text_properties.translation bpy.context.view_layer.objects.active = mom bpy.ops.object.parent_set(type='OBJECT') # endtime = time.perf_counter_ns() # elapsedtime = endtime - starttime return True # 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["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 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: 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 ""