From d56ca84236f9d7f0c9476179c2275e8036c397ae Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Sun, 18 May 2025 17:23:38 +0200 Subject: [PATCH] glyph properties, orientation fixes --- __init__.py | 67 +++++++- butils.py | 428 ++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 359 insertions(+), 136 deletions(-) diff --git a/__init__.py b/__init__.py index c49f2f5..8666c7a 100644 --- a/__init__.py +++ b/__init__.py @@ -134,11 +134,26 @@ class ABC3D_available_font(bpy.types.PropertyGroup): class ABC3D_glyph_properties(bpy.types.PropertyGroup): + + def update_callback(self, context): + if self.text_id >= 0: + butils.set_text_on_curve( + context.scene.abc3d_data.available_texts[self.text_id] + ) + glyph_id: bpy.props.StringProperty(maxlen=1) + text_id: bpy.props.IntProperty( + default=-1, + ) + alternate: bpy.props.IntProperty( + default=-1, + update=update_callback, + ) glyph_object: bpy.props.PointerProperty(type=bpy.types.Object) letter_spacing: bpy.props.FloatProperty( name="Letter Spacing", description="Letter Spacing", + update=update_callback, ) class ABC3D_text_properties(bpy.types.PropertyGroup): @@ -704,14 +719,38 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): def get_active_text_properties(self): # and bpy.context.object.select_get(): - if type(bpy.context.active_object) != type(None): - for t in bpy.context.scene.abc3d_data.available_texts: - if bpy.context.active_object == t.text_object: - return t - if bpy.context.active_object.parent == t.text_object: - return t + a_o = bpy.context.active_object + if a_o is not None: + if f"{utils.prefix()}_linked_textobject" in a_o: + text_index = a_o[f"{utils.prefix()}_linked_textobject"] + return bpy.context.scene.abc3d_data.available_texts[text_index] + elif f"{utils.prefix()}_linked_textobject" in a_o.parent: + text_index = a_o.parent[f"{utils.prefix()}_linked_textobject"] + return bpy.context.scene.abc3d_data.available_texts[text_index] + else: + for t in bpy.context.scene.abc3d_data.available_texts: + if butils.is_or_has_parent(bpy.context.active_object, t.text_object, max_depth=4): + return t return None + # NOTE: HERE + def get_active_glyph_properties(self): + a_o = bpy.context.active_object + if a_o is not None: + if (f"{utils.prefix()}_linked_textobject" in a_o + and f"{utils.prefix()}_glyph_index" in a_o): + text_index = a_o[f"{utils.prefix()}_linked_textobject"] + glyph_index = a_o[f"{utils.prefix()}_glyph_index"] + return bpy.context.scene.abc3d_data.available_texts[text_index].glyphs[glyph_index] + else: + for t in bpy.context.scene.abc3d_data.available_texts: + if butils.is_or_has_parent(a_o, t.text_object, if_is_parent=False, max_depth=4): + for g in t.glyphs: + if butils.is_or_has_parent(a_o, g.glyph_object, max_depth=4): + return g + return None + + # def font_items_callback(self, context): # items = [] # fonts = Font.get_loaded_fonts_and_faces() @@ -741,7 +780,7 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): @classmethod def poll(self, context): - return type(self.get_active_text_properties(self)) != type(None) + return self.get_active_text_properties(self) is not None def draw(self, context): layout = self.layout @@ -750,11 +789,16 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): abc3d_data = scene.abc3d_data props = self.get_active_text_properties() + glyph_props = self.get_active_glyph_properties() - if type(props) == type(None) or type(props.text_object) == type(None): + if props is None or props.text_object is None: # this should not happen # as then polling does not work # however, we are paranoid + if props is None: + layout.label(text="props is none") + elif props.text_object is None: + layout.label(text="props.text_object is none") return layout.label(text=f"Mom: {props.text_object.name}") @@ -768,6 +812,13 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): layout.column().prop(props, "translation") layout.column().prop(props, "orientation") + if glyph_props is None: + return + box = layout.box() + box.label(text=f"{glyph_props.glyph_id}") + box.row().prop(glyph_props, "letter_spacing") + + class ABC3D_OT_InstallFont(bpy.types.Operator): """Install or load Fontfile from path above. diff --git a/butils.py b/butils.py index 76cd32c..94ed804 100644 --- a/butils.py +++ b/butils.py @@ -733,6 +733,12 @@ def get_glyph_advance(glyph_obj): 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: @@ -802,6 +808,41 @@ def will_regenerate(text_properties): 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 + for i in range(0, max_depth): + o = o.parent + if o == parent: + return True + if o 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): """set_text_on_curve @@ -815,8 +856,9 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) :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 """ - - global lock_depsgraph_update_n_times + # 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 @@ -825,26 +867,41 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) 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 + # 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 = will_regenerate(text_properties) # 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() + mom[f"{utils.prefix()}_type"] = "textobject" + 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 + curve_length = get_curve_length(mom) advance = text_properties.offset glyph_advance = 0 + glyph_index = 0 is_command = False previous_spline_index = -1 @@ -860,7 +917,6 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) is_newline = True 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" ) @@ -870,9 +926,14 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) is_command = False glyph_id = c - glyph_tmp = Font.get_glyph( - text_properties.font_name, text_properties.face_name, glyph_id - ) + 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: @@ -887,6 +948,7 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) text_properties.font_name, text_properties.face_name, possible_replacement, + -1 ) if glyph_tmp is not None: message = message + f" (replaced with '{possible_replacement}')" @@ -903,59 +965,82 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) glyph = glyph_tmp.original - ob = None - obg = None + ############### GLYPH PROPERTIES + + glyph_properties = text_properties.glyphs[glyph_index] if not regenerate else text_properties.glyphs.add() + if regenerate: - ob = bpy.data.objects.new(f"{glyph_id}", None) - ob.hide_viewport = True - obg = bpy.data.objects.new(f"{glyph_id}_mesh", glyph.data) - ob[f"{utils.prefix()}_type"] = "glyph" - 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 + glyph_properties["glyph_id"] = glyph_id + glyph_properties["text_id"] = text_properties.text_id + glyph_properties["letter_spacing"] = 0 + + ############### 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) + outer_node[f"{utils.prefix()}_type"] = "glyph" + outer_node[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id + outer_node[f"{utils.prefix()}_glyph_index"] = glyph_index + outer_node[f"{utils.prefix()}_font_name"] = text_properties.font_name + outer_node[f"{utils.prefix()}_face_name"] = text_properties.face_name + + # Add into the scene. + mom.users_collection[0].objects.link(outer_node) + mom.users_collection[0].objects.link(inner_node) + # bpy.context.scene.collection.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) + + glyph_properties["glyph_object"] = outer_node else: - ob = text_properties.glyphs[i].glyph_object - for c in ob.children: + 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"): - obg = c + 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 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" + 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 = 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_ob_rotation_mode = None - previous_obg_rotation_mode = None - if ob.rotation_mode != "QUATERNION": - ob.rotation_mode = "QUATERNION" - previous_ob_rotation_mode = ob.rotation_mode - if obg.rotation_mode != "QUATERNION": - obg.rotation_mode = "QUATERNION" - previous_obg_rotation_mode = obg.rotation_mode + 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 - location, tangent, spline_index = calc_point_on_bezier_curve( - mom, advance, True, True - ) + # get info from bezier + location, tangent, spline_index = calc_point_on_bezier_curve(mom, advance, True, True) + + # check if we are on a new line if spline_index != previous_spline_index: is_newline = True - if regenerate: - # ob.location = mom.matrix_world @ ( - # location + text_properties.translation - # ) - ob.location = location + text_properties.translation - mom.users_collection[0].objects.link(obg) - mom.users_collection[0].objects.link(ob) - ob.parent = mom - obg.parent = ob - obg.location = mathutils.Vector((0.0, 0.0, 0.0)) - else: - ob.location = location + text_properties.translation + # position + outer_node.location = location + text_properties.translation # orientation / rotation mask = [0] @@ -969,26 +1054,28 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) q = mathutils.Quaternion() q.rotate(text_properties.orientation) - ob.rotation_quaternion = (motor[0].to_3x3() @ q.to_matrix()).to_quaternion() - # if regenerate: - # obg.rotation_quaternion = q - # ob.rotation_quaternion = ( - # mom.matrix_world @ motor[0] - # ).to_quaternion() - # else: - # ob.rotation_quaternion = motor[0].to_quaternion() + outer_node.rotation_quaternion = (motor[0].to_3x3() @ q.to_matrix()).to_quaternion() - # NOTE: supercool but out of scope, as we wouldhave to update it everytime the curve object rotates, - # but this would ignore the curve objects orientation: - # ob.rotation_quaternion = (mom.matrix_world.inverted().to_3x3() @ 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() - if previous_ob_rotation_mode: - ob.rotation_mode = previous_ob_rotation_mode - if previous_obg_rotation_mode: - obg.rotation_mode = previous_obg_rotation_mode + # # 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 + outer_node.hide_set(True) + + ############### PREPARE FOR THE NEXT + + print(f"{glyph_id}: {glyph_properties.letter_spacing=}") glyph_advance = ( - get_glyph_advance(glyph) * scalor + text_properties.letter_spacing + glyph_post_advance * scalor + text_properties.letter_spacing + glyph_properties.letter_spacing ) # now we need to compensate for curvature @@ -997,7 +1084,7 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) curve_compensation = 0 if distribution_type == "CALCULATE" and ( not is_newline or spline_index == 0 - ): # TODO: fix newline hack + ): if text_properties.compensate_curvature and glyph_advance > 0: previous_location, psi = calc_point_on_bezier_curve( mom, advance, False, True @@ -1027,67 +1114,46 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) output_spline_index=True, ) - ob.scale = (scalor, scalor, scalor) - advance = advance + glyph_advance + curve_compensation + glyph_index += 1 previous_spline_index = spline_index - if regenerate: - glyph_data = text_properties.glyphs.add() - glyph_data.glyph_id = glyph_id - glyph_data.glyph_object = ob - glyph_data.letter_spacing = 0 - - if regenerate: - mom[f"{utils.prefix()}_type"] = "textobject" - 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 - - if lock_depsgraph_update_n_times < 0: - lock_depsgraph_update_n_times = len( - bpy.context.selected_objects - ) - else: - lock_depsgraph_update_n_times += len( - bpy.context.selected_objects - ) - - # NOTE: we reset with a timeout, as setting and resetting certain things - # in fast succession will cause visual glitches (e.g. {}.data.use_path). - def reset(): - mom.data.use_path = previous_use_path - if counted_reset in bpy.app.handlers.depsgraph_update_post: - bpy.app.handlers.depsgraph_update_post.remove(counted_reset) - if bpy.app.timers.is_registered(reset): - bpy.app.timers.unregister(reset) - - molotov = reset_depsgraph_n + 0 - - def counted_reset(scene, depsgraph): - nonlocal molotov - if molotov == 0: - reset() - else: - molotov -= 1 - - # unregister previous resets to avoid multiple execution - if bpy.app.timers.is_registered(reset): - bpy.app.timers.unregister(reset) - if counted_reset in bpy.app.handlers.depsgraph_update_post: - bpy.app.handlers.depsgraph_update_post.remove(counted_reset) - - if not isinstance(reset_timeout_s, bool): - if reset_timeout_s > 0: - bpy.app.timers.register(reset, first_interval=reset_timeout_s) - elif reset_timeout <= 0: - reset() - - bpy.app.handlers.depsgraph_update_post.append(counted_reset) + # NOTE: depsgraph update not locked + # as we fixed data_path with parent_to_curve trick + # if lock_depsgraph_update_n_times < 0: + # lock_depsgraph_update_n_times = len( + # bpy.context.selected_objects + # ) + # else: + # lock_depsgraph_update_n_times += len( + # bpy.context.selected_objects + # ) + # # NOTE: we reset with a timeout, as setting and resetting certain things + # # in fast succession will cause visual glitches (e.g. {}.data.use_path). + # def reset(): + # mom.data.use_path = previous_use_path + # if counted_reset in bpy.app.handlers.depsgraph_update_post: + # bpy.app.handlers.depsgraph_update_post.remove(counted_reset) + # if bpy.app.timers.is_registered(reset): + # bpy.app.timers.unregister(reset) + # molotov = reset_depsgraph_n + 0 + # def counted_reset(scene, depsgraph): + # nonlocal molotov + # if molotov == 0: + # reset() + # else: + # molotov -= 1 + # # unregister previous resets to avoid multiple execution + # if bpy.app.timers.is_registered(reset): + # bpy.app.timers.unregister(reset) + # if counted_reset in bpy.app.handlers.depsgraph_update_post: + # bpy.app.handlers.depsgraph_update_post.remove(counted_reset) + # if not isinstance(reset_timeout_s, bool): + # if reset_timeout_s > 0: + # bpy.app.timers.register(reset, first_interval=reset_timeout_s) + # elif reset_timeout <= 0: + # reset() + # bpy.app.handlers.depsgraph_update_post.append(counted_reset) # endtime = time.perf_counter_ns() # elapsedtime = endtime - starttime @@ -1504,3 +1570,109 @@ def align_metrics_of_objects(objects=None): 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 ""