[feature] unload glyphs

This commit is contained in:
jrkb 2025-05-31 16:13:16 +02:00
parent 9423659153
commit 2dcd4e7a2c
3 changed files with 552 additions and 280 deletions

576
butils.py
View file

@ -4,6 +4,7 @@ import queue
import re
import bpy
import bpy_types
import mathutils
# import time # for debugging performance
@ -45,12 +46,14 @@ def apply_all_transforms(obj):
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
# broken
# def get_parent_collection_names(collection, parent_names):
# for parent_collection in bpy.data.collections:
# if collection.name in parent_collection.children.keys():
# parent_names.append(parent_collection.name)
# get_parent_collection_names(parent_collection, parent_names)
# return
def get_key(key):
return f"{utils.prefix()}_{key}"
@ -139,15 +142,15 @@ def calc_tangent_on_bezier(bezier_point_1, bezier_point_2, t):
# class TestCalcPoint():
# co: mathutils.Vector
# handle_left: mathutils.Vector
# handle_right: mathutils.Vector
# def __init__(self, co, handle_left=None, handle_right=None):
# self.co = co
# if handle_left is not None:
# self.handle_left = handle_left
# if handle_right is not None:
# self.handle_right = handle_right
# co: mathutils.Vector
# handle_left: mathutils.Vector
# handle_right: mathutils.Vector
# def __init__(self, co, handle_left=None, handle_right=None):
# self.co = co
# if handle_left is not None:
# self.handle_left = handle_left
# if handle_right is not None:
# self.handle_right = handle_right
# a = TestCalcPoint(mathutils.Vector((0,0,0)), handle_right=mathutils.Vector((0,1,0)))
@ -157,7 +160,6 @@ def calc_tangent_on_bezier(bezier_point_1, bezier_point_2, t):
# calc_point_on_bezier(a,b,0.5)
def align_rotations_auto_pivot(
mask, input_rotations, vectors, factors, local_main_axis
):
@ -251,7 +253,7 @@ def calc_point_on_bezier_spline(
# 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"
f"{utils.prefix()}::butils::calc_point_on_bezier_spline: whoops, no points. panicking. return 0,0,0"
)
if output_tangent:
return mathutils.Vector((0, 0, 0)), mathutils.Vector((1, 0, 0))
@ -275,8 +277,9 @@ def calc_point_on_bezier_spline(
# if the bezier points sit on each other we have same issue
# but that is then to be fixed in the bezier
if p.handle_left == p.co and len(bezier_spline_obj.bezier_points) > 1:
beziers, lengths, total_length = get_real_beziers_and_lengths(bezier_spline_obj,
resolution_factor)
beziers, lengths, total_length = get_real_beziers_and_lengths(
bezier_spline_obj, resolution_factor
)
travel_point = calc_point_on_bezier(beziers[0][1], beziers[0][0], 0.001)
travel = travel_point.normalized() * distance
@ -288,8 +291,9 @@ def calc_point_on_bezier_spline(
else:
return location
beziers, lengths, total_length = get_real_beziers_and_lengths(bezier_spline_obj,
resolution_factor)
beziers, lengths, total_length = get_real_beziers_and_lengths(
bezier_spline_obj, resolution_factor
)
iterated_distance = 0
for i in range(0, len(beziers)):
@ -307,7 +311,7 @@ def calc_point_on_bezier_spline(
# if we are here, the point is outside the spline
last_i = len(beziers) - 1
p = beziers[last_i][1]
travel = (p.handle_right - p.co).normalized() * (distance - total_length)
# in case the handles sit on the points
@ -368,6 +372,7 @@ def calc_point_on_bezier_curve(
# 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)]
@ -397,13 +402,14 @@ def find_objects_by_custom_property(objects, 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)
# not verified
# def turn_collection_hierarchy_into_path(obj):
# parent_collection = obj.users_collection[0]
# parent_names = []
# parent_names.append(parent_collection.name)
# get_parent_collection_names(parent_collection, parent_names)
# parent_names.reverse()
# return "\\".join(parent_names)
def find_font_object(fontcollection, font_name):
@ -458,7 +464,9 @@ def move_in_fontcollection(obj, fontcollection, allow_duplicates=False):
fontcollection.objects.link(glyphs_obj)
glyphs_obj.parent = face_obj
elif len(glyphs_objs) > 1:
print("found more glyphs objects than expected")
print(
f"{utils.prefix()}::move_in_fontcollection: found more glyphs objects than expected"
)
# now it must exist
glyphs_obj = find_objects_by_name(face_obj.children, startswith="glyphs")[0]
@ -563,10 +571,14 @@ def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""):
for mff in modified_font_faces:
mff_glyphs = []
face : Font.FontFace = Font.get_font_face(mff["font_name"], mff["face_name"])
face: Font.FontFace = Font.get_font_face(mff["font_name"], mff["face_name"])
if face is None:
print(f"{utils.prefix()}::load_font_from_path({filepath=}, {glyphs=}, {font_name=}, {face_name=}) failed")
print(f"modified font face {mff=} could not be accessed.")
print(
f"{utils.prefix()}::load_font_from_path({filepath=}, {glyphs=}, {font_name=}, {face_name=}) failed"
)
print(
f"{utils.prefix()}:: modified font face {mff=} could not be accessed."
)
continue
# iterate glyphs
for g in face.glyphs:
@ -592,6 +604,143 @@ def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""):
# completely_delete_objects(remove_list)
def is_glyph_used(glyph_alternates):
fontcollection: bpy_types.Collection = bpy.data.collections.get("ABC3D")
glyph = bpy.types.PointerProperty
for glyph in glyph_alternates:
for o in bpy.context.scene.objects:
# only check other glyphs
if is_glyph_object(o):
# then attempt to compare properties
if (
get_key("font_name") in o
and get_key("face_name") in o
and get_key("glyph_id") in o
and o[get_key("font_name")] == glyph["font_name"]
and o[get_key("face_name")] == glyph["face_name"]
and o[get_key("glyph_id")] == glyph["glyph"]
):
# following check is not necessary,
# but we leave it in for backwards compatibility
# properties in the fontcollection start with prefix
# and so they should be caught by previous check
if fontcollection.users == 0 or not (
fontcollection in o.users_collection
and len(o.users_collection) <= 1
):
# it's in the scene and has the correct properties
# it is used
return True
# following check is possibly overkill
# but we also check for objects that use the data
# and are not glyph objects, in that case we don't pull the data
# from under their feet
if is_mesh(o) and o.data == glyph.data:
# in this case, yes we need to check if it is a glyph in the fontcollection
if fontcollection.users == 0 or not (
fontcollection in o.users_collection
and len(o.users_collection) <= 1
):
# bam!
return True
# whoosh!
return False
def clean_fontcollection(fontcollection=None):
if fontcollection is None:
fontcollection = bpy.data.collections.get("ABC3D")
if fontcollection is None:
print(
f"{utils.prefix()}::clean_fontcollection: failed beacause fontcollection is none"
)
return False
collection_fonts = find_objects_by_custom_property(
fontcollection.all_objects, "is_font", True
)
delete_these_fonts = []
delete_these_font_faces = []
delete_these_glyph_moms = []
for font_and_face in Font.get_loaded_fonts_and_faces():
font_name = font_and_face[0]
face_name = font_and_face[1]
collection_font_list = find_objects_by_custom_property(
collection_fonts, "font_name", font_name
)
for collection_font in collection_font_list:
collection_font_face_list = find_objects_by_custom_property(
collection_font.children, "face_name", face_name
)
count_font_faces = 0
for collection_font_face in collection_font_face_list:
glyphs_mom_list = find_objects_by_name(
collection_font_face.children, startswith="glyphs"
)
count_glyphs_moms = 0
for glyphs_mom in glyphs_mom_list:
if len(glyphs_mom.children) == 0:
delete_these_glyph_moms.append(glyphs_mom)
count_glyphs_moms += 1
if len(collection_font_face.children) == count_glyphs_moms:
delete_these_font_faces.append(collection_font_face)
count_font_faces += 1
if len(collection_font.children) == count_font_faces:
delete_these_fonts.append(collection_font)
completely_delete_objects(delete_these_glyph_moms)
completely_delete_objects(delete_these_font_faces)
completely_delete_objects(delete_these_fonts)
def unload_unused_glyph(font_name, face_name, glyph_id, do_clean_fontcollection=True):
fontcollection: bpy_types.Collection = bpy.data.collections.get("ABC3D")
glyph_variations = Font.get_glyphs(font_name, face_name, glyph_id)
if is_glyph_used(glyph_variations):
return False
delete_these = []
for glyph_pointer in glyph_variations:
for o in fontcollection.all_objects:
if (
is_glyph_object(o)
and o["font_name"] == font_name
and o["face_name"] == face_name
and o["glyph"] == glyph_id
):
if len(o.users_collection) <= 1:
delete_these.append(o)
completely_delete_objects(delete_these)
Font.unloaded_glyph(font_name, face_name, glyph_id)
if do_clean_fontcollection:
clean_fontcollection(fontcollection)
return True
def unload_unused_glyphs(do_clean_fontcollection=True):
fontcollection: bpy_types.Collection = bpy.data.collections.get("ABC3D")
if fontcollection is not None:
for font_and_face in Font.get_loaded_fonts_and_faces():
font_name = font_and_face[0]
face_name = font_and_face[1]
face: Font.FontFace | None = Font.get_font_face(font_name, face_name)
if face is None:
print(
f"{utils.prefix()}::unload_unused_glyphs: face is None {font_name=} {face_name=}"
)
continue
unloaded_these = []
for glyph_id in face.loaded_glyphs.copy():
unload_unused_glyph(
font_name, face_name, glyph_id, do_clean_fontcollection=False
)
if do_clean_fontcollection:
clean_fontcollection(fontcollection)
def update_available_fonts():
abc3d_data = bpy.context.scene.abc3d_data
@ -606,7 +755,9 @@ def update_available_fonts():
f = abc3d_data.available_fonts.add()
f.font_name = font_name
f.face_name = face_name
print(f"{utils.prefix()}::update_available_fonts: {__name__} added {font_name} {face_name}")
print(
f"{utils.prefix()}::update_available_fonts: {__name__} added {font_name} {face_name}"
)
# def update_available_texts():
@ -724,6 +875,8 @@ def completely_delete_objects(objs, recursive=True):
except ReferenceError:
# not important
pass
except RuntimeError:
pass
def is_mesh(o):
@ -753,7 +906,7 @@ def is_glyph_object(o):
return o[f"{utils.prefix()}_type"] == "glyph"
try:
return (
type(o.parent) is not type(None)
o.parent is not None
and "glyphs" in o.parent.name
and is_mesh(o)
and not is_metrics_object(o)
@ -792,6 +945,7 @@ 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):
@ -807,14 +961,16 @@ def get_glyph_height(glyph_obj):
def prepare_text(font_name, face_name, text, allow_replacement=True):
availability = Font.test_glyphs_availability(
font_name, face_name, text
)
availability = Font.test_glyphs_availability(font_name, face_name, text)
if isinstance(availability, int):
if availability == Font.MISSING_FONT:
print(f"{utils.prefix()}::prepare_text({font_name=}, {face_name=}, {text=}) failed with MISSING_FONT")
print(
f"{utils.prefix()}::prepare_text({font_name=}, {face_name=}, {text=}) failed with MISSING_FONT"
)
if availability is Font.MISSING_FACE:
print(f"{utils.prefix()}::prepare_text({font_name=}, {face_name=}, {text=}) failed with MISSING_FACE")
print(
f"{utils.prefix()}::prepare_text({font_name=}, {face_name=}, {text=}) failed with MISSING_FACE"
)
return False
loadable = availability.unloaded
# possibly replace upper and lower case letters with each other
@ -832,9 +988,16 @@ def prepare_text(font_name, face_name, text, allow_replacement=True):
load_font_from_filepath(filepath, loadable, font_name, face_name)
return True
def predict_actual_text(text_properties):
availability = Font.test_availability(text_properties.font_name, text_properties.face_name, text_properties.text)
AVAILABILITY = Font.test_availability(text_properties.font_name, text_properties.face_name, text_properties.text.swapcase())
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, "")
@ -842,6 +1005,7 @@ def predict_actual_text(text_properties):
t_text = t_text.replace(c, "")
return t_text
def is_bezier(curve):
if curve.type != "CURVE":
return False
@ -906,6 +1070,7 @@ 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
@ -922,21 +1087,27 @@ def find_free_text_id():
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_property = (
text_properties[key]
if key in text_properties
else getattr(text_properties, key)
)
text_object_property = o[object_key] if object_key in o else False
if text_property != text_object_property:
if key in keys_trigger_regeneration:
return COMPARE_TEXT_OBJECT_REGENERATE
elif key in ["translation", "orientation"]:
if (
text_property[0] != text_object_property[0] or
text_property[1] != text_object_property[1] or
text_property[2] != text_object_property[2]):
text_property[0] != text_object_property[0]
or text_property[1] != text_object_property[1]
or text_property[2] != text_object_property[2]
):
return COMPARE_TEXT_OBJECT_DIFFER
# else same
else:
@ -950,16 +1121,17 @@ def transfer_text_properties_to_text_object(text_properties, o):
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_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)
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:
@ -973,7 +1145,7 @@ def get_glyph(glyph_id, font_name, face_name, notify_on_replacement=False):
text_properties.font_name,
text_properties.face_name,
possible_replacement,
-1
-1,
)
if glyph_tmp is not None:
message = message + f" (replaced with '{possible_replacement}')"
@ -991,7 +1163,8 @@ def get_glyph(glyph_id, font_name, face_name, notify_on_replacement=False):
return glyph_tmp.original
def get_text_properties(text_id, scene = None):
def get_text_properties(text_id, scene=None):
if scene is None:
scene = bpy.context.scene
abc3d_data = scene.abc3d_data
@ -1000,7 +1173,15 @@ def get_text_properties(text_id, scene = None):
return t
return None
def duplicate(obj, data=True, actions=True, add_to_collection=True, collection=None, recursive=True):
def duplicate(
obj,
data=True,
actions=True,
add_to_collection=True,
collection=None,
recursive=True,
):
obj_copy = obj.copy()
if add_to_collection:
if collection:
@ -1019,8 +1200,13 @@ def duplicate(obj, data=True, actions=True, add_to_collection=True, collection=N
# child_copy.matrix_parent_inverse = obj_copy.matrix_world.inverted()
return obj_copy
def transfer_text_object_to_text_properties(text_object, text_properties, id_from_text_properties=True):
possible_brother_text_id = text_object[get_key("text_id")] if get_key("text_id") in text_object else ""
def transfer_text_object_to_text_properties(
text_object, text_properties, id_from_text_properties=True
):
possible_brother_text_id = (
text_object[get_key("text_id")] if get_key("text_id") in text_object else ""
)
for key in text_object_keys:
if key in ignore_keys_in_text_object_comparison:
continue
@ -1028,12 +1214,17 @@ def transfer_text_object_to_text_properties(text_object, text_properties, id_fro
if id_from_text_properties and key == "text_id":
text_object[object_key] = text_properties["text_id"]
else:
text_object_property = text_object[object_key] if object_key in text_object else False
text_object_property = (
text_object[object_key] if object_key in text_object else False
)
if text_object_property is not False:
text_properties[key] = text_object_property
if len(text_object.children) == 0:
if possible_brother_text_id != text_properties["text_id"] and possible_brother_text_id != "":
if (
possible_brother_text_id != text_properties["text_id"]
and possible_brother_text_id != ""
):
possible_brother_properties = get_text_properties(possible_brother_text_id)
possible_brother_object = possible_brother_properties.text_object
if possible_brother_object is not None:
@ -1047,11 +1238,7 @@ def transfer_text_object_to_text_properties(text_object, text_properties, id_fro
found_reconstructable_glyphs = False
glyph_objects_with_indices = []
required_keys = [
"glyph_index",
"glyph_id",
"type"
]
required_keys = ["glyph_index", "glyph_id", "type"]
for glyph_object in text_object.children:
if is_glyph_object(glyph_object):
has_required_keys = True
@ -1087,8 +1274,8 @@ def transfer_text_object_to_text_properties(text_object, text_properties, id_fro
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)
# text_properties.face_name,
# glyph_id)
# glyph = glyph_tmp.original
glyph_properties = text_properties.glyphs.add()
@ -1111,8 +1298,10 @@ def transfer_text_object_to_text_properties(text_object, text_properties, id_fro
text_properties.glyphs.clear()
unfortunate_children = text_object.children
completely_delete_objects(unfortunate_children)
def kill_children():
completely_delete_objects(unfortunate_children)
run_in_main_thread(kill_children)
if "font_name" in text_properties and "face_name" in text_properties:
@ -1128,9 +1317,11 @@ def link_text_object_with_new_text_properties(text_object, scene=None):
text_properties = scene.abc3d_data.available_texts.add()
text_properties["text_id"] = text_id
# text_object[get_key("text_id")] = text_id
prepare_text(text_object[get_key("font_name")],
text_object[get_key("face_name")],
text_object[get_key("text")])
prepare_text(
text_object[get_key("font_name")],
text_object[get_key("face_name")],
text_object[get_key("text")],
)
text_properties.text_object = text_object
transfer_text_object_to_text_properties(text_object, text_properties)
@ -1144,14 +1335,14 @@ def test_finding():
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 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):
@ -1159,22 +1350,33 @@ def link_text_object_and_text_properties(o, text_properties):
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)
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):
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[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:
@ -1182,6 +1384,7 @@ def transfer_glyph_object_to_glyph_properties(glyph_object, glyph_properties):
glyph_properties[key] = glyph_object[get_key(key)]
glyph_properties["text_id"] = glyph_object[get_key("text_id")]
def would_regenerate(text_properties):
predicted_text = predict_actual_text(text_properties)
if text_properties.actual_text != predicted_text:
@ -1217,10 +1420,11 @@ 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
# 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:
@ -1234,23 +1438,26 @@ def is_or_has_parent(o, parent, if_is_parent=True, max_depth=10):
return False
return False
def parent_to_curve(o, c):
o.parent_type = 'OBJECT'
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':
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):
def set_text_on_curve(
text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, can_regenerate=False
):
"""set_text_on_curve
An earlier reset cancels the other.
@ -1273,7 +1480,6 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4,
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
@ -1283,9 +1489,9 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4,
# https://projects.blender.org/blender/blender/issues/100661
# previous_use_path = mom.data.use_path
# if distribution_type == "CALCULATE":
# mom.data.use_path = False
# mom.data.use_path = False
# elif distribution_type == "FOLLOW_PATH":
# mom.data.use_path = True
# mom.data.use_path = True
regenerate = can_regenerate and would_regenerate(text_properties)
@ -1330,10 +1536,9 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4,
############### GET GLYPH
glyph_tmp = Font.get_glyph(text_properties.font_name,
text_properties.face_name,
glyph_id,
-1)
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:
@ -1348,7 +1553,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
-1,
)
if glyph_tmp is not None:
message = message + f" (replaced with '{possible_replacement}')"
@ -1368,7 +1573,11 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4,
############### GLYPH PROPERTIES
glyph_properties = text_properties.glyphs[glyph_index] if not regenerate else text_properties.glyphs.add()
glyph_properties = (
text_properties.glyphs[glyph_index]
if not regenerate
else text_properties.glyphs.add()
)
if regenerate:
glyph_properties["glyph_id"] = glyph_id
@ -1383,14 +1592,16 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4,
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)
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_type = "OBJECT"
inner_node.parent = outer_node
inner_node.matrix_parent_inverse = outer_node.matrix_world.inverted()
parent_to_curve(outer_node, mom)
@ -1426,7 +1637,9 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4,
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"].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"
@ -1442,7 +1655,9 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4,
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)
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:
@ -1457,13 +1672,19 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4,
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()]
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()
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:
@ -1482,16 +1703,16 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4,
############### PREPARE FOR THE NEXT
glyph_advance = (
glyph_post_advance * scalor + text_properties.letter_spacing + glyph_properties.letter_spacing
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 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
@ -1503,8 +1724,10 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4,
n_max = 100
n = 0
while (
previous_location - new_location
).length > glyph_advance and psi == si and n < n_max:
(previous_location - new_location).length > glyph_advance
and psi == si
and n < n_max
):
curve_compensation = curve_compensation - glyph_advance * 0.01
tmp_new_location, si = calc_point_on_bezier_curve(
mom,
@ -1513,14 +1736,18 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4,
output_spline_index=True,
)
if tmp_new_location == new_location:
print(f"{utils.prefix()}::set_text_on_curve::compensate_curvature while loop overstaying welcome")
print(
f"{utils.prefix()}::set_text_on_curve::compensate_curvature while loop overstaying welcome"
)
break
new_location = tmp_new_location
n += 1
n = 0
while (
previous_location - new_location
).length < glyph_advance and psi == si and n < n_max:
(previous_location - new_location).length < glyph_advance
and psi == si
and n < n_max
):
curve_compensation = curve_compensation + glyph_advance * 0.01
tmp_new_location, si = calc_point_on_bezier_curve(
mom,
@ -1529,7 +1756,9 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4,
output_spline_index=True,
)
if tmp_new_location == new_location:
print(f"{utils.prefix()}::set_text_on_curve::compensate_curvature while loop overstaying welcome")
print(
f"{utils.prefix()}::set_text_on_curve::compensate_curvature while loop overstaying welcome"
)
break
new_location = tmp_new_location
n += 1
@ -1813,6 +2042,8 @@ def add_default_metrics_to_objects(objects=None, overwrite_existing=False):
targets = []
reference_bound_box = None
for o in objects:
if not hasattr(o, "parent"):
print(f"{o.name} has not a PARENTNTNTNTNTNNTNTNTNTNTN")
is_possibly_glyph = is_glyph(o)
if is_possibly_glyph:
metrics = []
@ -1932,6 +2163,7 @@ 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
@ -1954,87 +2186,87 @@ def align_origins_to_active_object(objects=None, axis=2):
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)])
# 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
# 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
# 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"
# 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 ""
# 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"
# 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]
# 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]
# metrics_origin_x = c.matrix_world.translation[0] + min_x
# for v in o.data.vertices:
# v.co[0] -= diff
# diff = metrics_origin_x - o.matrix_world.translation[0]
# o.location += mathutils.Vector((diff, 0.0, 0.0)) @ o.matrix_world.inverted()
# for v in o.data.vertices:
# v.co[0] -= diff
# for c in o.children:
# if is_metrics_object(c):
# c.location -= mathutils.Vector((diff, 0.0, 0.0)) @ o.matrix_world.inverted()
# o.location += mathutils.Vector((diff, 0.0, 0.0)) @ o.matrix_world.inverted()
# return ""
# for c in o.children:
# if is_metrics_object(c):
# c.location -= mathutils.Vector((diff, 0.0, 0.0)) @ o.matrix_world.inverted()
# return ""