import importlib import os import queue import re import bpy 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() 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 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() 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) 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 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, output_spline_index=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: 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 ] 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("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.fonts[mff["font_name"]].faces[mff["face_name"]] # 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 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 "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 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 ( 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: 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): loaded, missing, loadable, files = Font.test_glyphs_availability( font_name, face_name, text ) # possibly replace upper and lower case letters with each other if len(missing) > 0 and allow_replacement: replacement_search = "" for m in missing: if m.isalpha(): replacement_search += m.swapcase() r = Font.test_availability(font_name, face_name, replacement_search) loadable += r["maybe"] # not update (loaded, missing, files), we only use loadable/maybe later if len(loadable) > 0: for filepath in files: load_font_from_filepath(filepath, loadable, font_name, face_name) return True 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: print(f"{key}: REGENERATE {text_property=} {text_object_property}") 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]): print(f"{key}: DIFFER {text_property=} {text_object_property}") return COMPARE_TEXT_OBJECT_DIFFER else: print(f"{key}: SAME {text_property.xyz=} {text_object_property.to_list()}") else: print(f"{key}: DIFFER {text_property=} {text_object_property}") return COMPARE_TEXT_OBJECT_DIFFER else: print(f"{key}: SAME {text_property=} {text_object_property}") 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 transfer_text_object_to_text_properties(text_object, text_properties, id_from_text_properties=True): print("TRANSFER:: BEGIN") print(f" {text_properties['text_id']=}") print(f" {type(text_object)=}") 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"] print(f" {object_key} <= {key}") else: text_object_property = text_object[object_key] if object_key in text_object else False if text_object_property is not False: print(f" {object_key} => {key}") text_properties[key] = text_object_property print(f" {dict(text_properties)=}") print(f" {text_properties['offset']=}") if len(text_object.children) == 0: print("could be duplicate?") if possible_brother_text_id != text_properties["text_id"] and possible_brother_text_id != "": pass 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")]) print(f" {glyph_objects_with_indices=}") 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 print(f"{text=} is a good text because it is the same") else: 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"]: t_text = t_text.replace(c, "") for c in AVAILABILITY["missing"]: t_text = t_text.replace(c, "") if len(t_text) == len(text): print(f"{text=} is a good text because it is the same considering what is possible") is_good_text = True if is_good_text: print(" GOOD TEXT") # for glyph_index, glyph_object in enumerate(glyph_objects_with_indices): # print(f"{glyph_index}: {glyph_object}") # if glyph_index == glyph_object[get_key("glyph_index")]: # print("yeey glyph_index matches") # else: # print("nooo glyph_idex macthes not") # found_reconstructable_glyphs = True 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 glyph_properties["glyph_index"] = glyph_index inner_node = None for c in glyph_object.children: if c.name.startswith(f"{glyph_id}_mesh"): inner_node = c print(f"found inner node {inner_node.name=} for {glyph_id=}") if inner_node is None: fail_after_all = True pass glyph_properties["glyph_object"] = glyph_object if not fail_after_all: found_reconstructable_glyphs = True # gp = text_properties.glyphs[i] # if gp["glyph_id"] == g["glyph_id"] or gp["glyph_id"] == g["glyph_id"].swapcase(): # if "alternate" in g: # gp["alternate"] = g["alternate"] # for key in glyph_object_keys: # if key in ignore_keys_in_glyph_object_comparison: # continue # object_key = get_key(key) # if object_key in g: # gp[key] = g[object_key] # else: # text_properties.glyphs.clear() # # for g in found_glyphs_with_indices: # # i = g["glyph_index"] # # gp = text_properties.glyphs.add() # # if gp["glyph_id"] == g["glyph_id"] or gp["glyph_id"] == g["glyph_id"].swapcase(): # # if "alternate" in g: # # gp["alternate"] = g["alternate"] # # for key in glyph_object_keys: # # if key in ignore_keys_in_glyph_object_comparison: # # continue # # object_key = get_key(key) # # if object_key in g: # # gp[key] = g[object_key] if not found_reconstructable_glyphs: print("KILL THE GLYPHS") text_properties.actual_text = "" text_properties.glyphs.clear() unfortunate_children = text_object.children print("KILL THE CHILDREN") completely_delete_objects(unfortunate_children) def kill_children(): print("KILL THE 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}" print("TRANSFER:: END") 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 print(f" found free {text_id=}") print(" preparing text") prepare_text(text_object[get_key("font_name")], text_object[get_key("face_name")], text_object[get_key("text")]) print(" prepared text, transferring text object") 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() print(type(t)) t["text_id"] = text_id print(t["text_id"]) o = bpy.context.active_object transfer_text_object_to_text_properties(o, t) # 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")] import inspect def would_regenerate(text_properties): print("REGENERATE?") if len(text_properties.actual_text) != len(text_properties.glyphs): print(inspect.currentframe().f_lineno) return True if len(text_properties.glyphs) == 0: print(inspect.currentframe().f_lineno) return True for i, g in enumerate(text_properties.glyphs): if not hasattr(g.glyph_object, "type"): print(inspect.currentframe().f_lineno) return True elif g.glyph_object.type != "EMPTY": print(inspect.currentframe().f_lineno) return True # check if perhaps one glyph was deleted elif g.glyph_object is None: print(inspect.currentframe().f_lineno) return True elif g.glyph_object.parent is None: print(inspect.currentframe().f_lineno) return True elif g.glyph_object.parent.users_collection != g.glyph_object.users_collection: print(inspect.currentframe().f_lineno) return True elif len(text_properties.text) > i and g.glyph_id != text_properties.text[i]: print(inspect.currentframe().f_lineno) 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 ): print(inspect.currentframe().f_lineno) return True print("NOT REGENERATE") 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): 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 set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, can_regenerate=False): # for i in range (0, 42): # print("WATCH OUT, WE DO NOT SET THE TEXT ATM") # return 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() mom = text_properties.text_object if mom.type != "CURVE": return False distribution_type = "CALCULATE" if is_bezier(mom) else "FOLLOW_PATH" # NOTE: following not necessary anymore # as we fixed data_path with parent_to_curve trick # # use_path messes with parenting # however, we need it for follow_path # https://projects.blender.org/blender/blender/issues/100661 # previous_use_path = mom.data.use_path # if distribution_type == "CALCULATE": # mom.data.use_path = False # elif distribution_type == "FOLLOW_PATH": # mom.data.use_path = True regenerate = can_regenerate and would_regenerate(text_properties) if regenerate: print("RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRREGENERATE") # if we regenerate.... delete objects if regenerate and text_properties.get("glyphs"): for g in text_properties.glyphs: print(dict(g)) glyph_objects = [g["glyph_object"] for g in text_properties["glyphs"]] completely_delete_objects(glyph_objects, True) text_properties.glyphs.clear() transfer_text_properties_to_text_object(text_properties, mom) curve_length = get_curve_length(mom) advance = text_properties.offset glyph_advance = 0 glyph_index = 0 is_command = False previous_spline_index = -1 actual_text = "" for i, c in enumerate(text_properties.text): face = Font.fonts[text_properties.font_name].faces[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 ############### GET GLYPH glyph_tmp = Font.get_glyph(text_properties.font_name, text_properties.face_name, glyph_id, -1) if glyph_tmp is None: space_width = Font.is_space(glyph_id) if space_width: advance = advance + space_width * text_properties.font_size continue message = f"Glyph not found for font_name='{text_properties.font_name}' face_name='{text_properties.face_name}' glyph_id='{glyph_id}'" replaced = False if glyph_id.isalpha(): possible_replacement = glyph_id.swapcase() glyph_tmp = Font.get_glyph( text_properties.font_name, text_properties.face_name, possible_replacement, -1 ) if glyph_tmp is not None: message = message + f" (replaced with '{possible_replacement}')" replaced = True if can_regenerate: ShowMessageBox( title="Glyph replaced" if replaced else "Glyph missing", icon="INFO" if replaced else "ERROR", message=message, prevent_repeat=True, ) if not replaced: continue glyph = glyph_tmp.original ############### GLYPH PROPERTIES glyph_properties = text_properties.glyphs[glyph_index] if not regenerate else text_properties.glyphs.add() if regenerate: glyph_properties["glyph_id"] = glyph_id glyph_properties["text_id"] = text_properties.text_id glyph_properties["letter_spacing"] = 0 actual_text += glyph_id ############### NODE SCENE MANAGEMENT inner_node = None outer_node = None if regenerate: outer_node = bpy.data.objects.new(f"{glyph_id}", None) inner_node = bpy.data.objects.new(f"{glyph_id}_mesh", glyph.data) transfer_properties_to_glyph_object(text_properties, glyph_properties, outer_node) # Add into the scene. mom.users_collection[0].objects.link(outer_node) mom.users_collection[0].objects.link(inner_node) # Parenting is hard. inner_node.parent_type = 'OBJECT' inner_node.parent = outer_node inner_node.matrix_parent_inverse = outer_node.matrix_world.inverted() parent_to_curve(outer_node, mom) outer_node.hide_set(True) glyph_properties["glyph_object"] = outer_node outer_node[f"{utils.prefix()}_glyph_index"] = glyph_index else: outer_node = glyph_properties.glyph_object outer_node[f"{utils.prefix()}_glyph_index"] = glyph_index for c in outer_node.children: if c.name.startswith(f"{glyph_id}_mesh"): inner_node = c ############### TRANSFORMS # 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_outer_node_rotation_mode = None previous_inner_node_rotation_mode = None if outer_node.rotation_mode != "QUATERNION": outer_node.rotation_mode = "QUATERNION" previous_outer_node_rotation_mode = outer_node.rotation_mode if inner_node.rotation_mode != "QUATERNION": inner_node.rotation_mode = "QUATERNION" previous_inner_node_rotation_mode = inner_node.rotation_mode # get info from bezier location, tangent, spline_index = calc_point_on_bezier_curve(mom, applied_advance, True, True) # check if we are on a new line if spline_index != previous_spline_index: is_newline = True # position outer_node.location = location + text_properties.translation # 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) outer_node.rotation_quaternion = (motor[0].to_3x3() @ q.to_matrix()).to_quaternion() # # NOTE: supercool but out of scope, as we wouldhave to update it everytime the curve object rotates, # # but this would ignore the curve objects orientation: # outer_node.rotation_quaternion = (mom.matrix_world.inverted().to_3x3() @ motor[0].to_3x3() @ q.to_matrix()).to_quaternion() # # scale outer_node.scale = (scalor, scalor, scalor) if previous_outer_node_rotation_mode: outer_node.rotation_mode = previous_outer_node_rotation_mode if previous_inner_node_rotation_mode: inner_node.rotation_mode = previous_inner_node_rotation_mode # 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: while ( previous_location - new_location ).length > glyph_advance and psi == si: curve_compensation = curve_compensation - glyph_advance * 0.01 new_location, si = calc_point_on_bezier_curve( mom, advance + glyph_advance + curve_compensation, output_tangent=False, output_spline_index=True, ) while ( previous_location - new_location ).length < glyph_advance and psi == si: curve_compensation = curve_compensation + glyph_advance * 0.01 new_location, si = calc_point_on_bezier_curve( mom, advance + glyph_advance + curve_compensation, output_tangent=False, output_spline_index=True, ) advance = advance + glyph_advance + curve_compensation glyph_index += 1 previous_spline_index = spline_index if regenerate: 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 ""