refactor ensure glyphs + alternates
This commit is contained in:
parent
14d1b7a160
commit
7de8fcc5d1
3 changed files with 332 additions and 129 deletions
353
butils.py
353
butils.py
|
@ -663,7 +663,7 @@ def clean_fontcollection(fontcollection=None):
|
|||
fontcollection = bpy.data.collections.get("ABC3D")
|
||||
if fontcollection is None:
|
||||
print(
|
||||
f"{utils.prefix()}::clean_fontcollection: failed beacause fontcollection is none"
|
||||
f"{utils.prefix()}::clean_fontcollection: failed because fontcollection is none"
|
||||
)
|
||||
return False
|
||||
|
||||
|
@ -1011,9 +1011,11 @@ def predict_actual_text(text_properties):
|
|||
)
|
||||
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, "")
|
||||
C = c.swapcase()
|
||||
if C in AVAILABILITY.missing:
|
||||
t_text = t_text.replace(c, "")
|
||||
else:
|
||||
t_text = t_text.replace(c, C)
|
||||
return t_text
|
||||
|
||||
|
||||
|
@ -1184,6 +1186,7 @@ def get_text_properties(text_id, scene=None):
|
|||
return t
|
||||
return None
|
||||
|
||||
|
||||
def get_text_properties_by_index(text_index, scene=None):
|
||||
if scene is None:
|
||||
scene = bpy.context.scene
|
||||
|
@ -1299,10 +1302,7 @@ def transfer_text_object_to_text_properties(
|
|||
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
|
||||
glyph_properties["text_id"] = text_properties.text_id
|
||||
glyph_object["text_id"] = text_properties.text_id
|
||||
# glyph_properties["glyph_object"] = glyph_object
|
||||
inner_node = None
|
||||
for c in glyph_object.children:
|
||||
if c.name.startswith(f"{glyph_id}_mesh"):
|
||||
|
@ -1311,6 +1311,9 @@ def transfer_text_object_to_text_properties(
|
|||
fail_after_all = True
|
||||
pass
|
||||
glyph_properties["glyph_object"] = glyph_object
|
||||
glyph_properties["glyph_index"] = glyph_index
|
||||
glyph_properties["text_id"] = text_properties.text_id
|
||||
glyph_object["text_id"] = text_properties.text_id
|
||||
if not fail_after_all:
|
||||
found_reconstructable_glyphs = True
|
||||
|
||||
|
@ -1421,10 +1424,21 @@ def transfer_glyph_object_to_glyph_properties(glyph_object, glyph_properties):
|
|||
glyph_properties["text_id"] = glyph_object[get_key("text_id")]
|
||||
|
||||
|
||||
def get_text_difference_index(text_a, text_b):
|
||||
len_a = len(text_a)
|
||||
len_b = len(text_b)
|
||||
len_min = min(len_a, len_b)
|
||||
len_max = max(len_a, len_b)
|
||||
for i in range(0, len_max):
|
||||
if i >= len_min or text_a[i] != text_b[i]:
|
||||
return i
|
||||
return False
|
||||
|
||||
|
||||
def would_regenerate(text_properties):
|
||||
predicted_text = predict_actual_text(text_properties)
|
||||
if text_properties.actual_text != predicted_text:
|
||||
return True
|
||||
return get_text_difference_index(text_properties.actual_text, predicted_text)
|
||||
if len(text_properties.glyphs) == 0:
|
||||
return True
|
||||
|
||||
|
@ -1476,6 +1490,7 @@ def is_or_has_parent(o, parent, if_is_parent=True, max_depth=10):
|
|||
|
||||
|
||||
def parent_to_curve(o, c):
|
||||
# https://projects.blender.org/blender/blender/issues/100661
|
||||
o.parent_type = "OBJECT"
|
||||
o.parent = c
|
||||
# o.matrix_parent_inverse = c.matrix_world.inverted()
|
||||
|
@ -1491,6 +1506,184 @@ def parent_to_curve(o, c):
|
|||
o.matrix_parent_inverse.translation = p * -1.0
|
||||
|
||||
|
||||
def get_original_glyph(text_properties, glyph_properties):
|
||||
glyph_tmp = Font.get_glyph(
|
||||
text_properties.font_name,
|
||||
text_properties.face_name,
|
||||
glyph_properties.glyph_id,
|
||||
glyph_properties.alternate,
|
||||
)
|
||||
if glyph_tmp is None:
|
||||
return None
|
||||
return glyph_tmp.original
|
||||
|
||||
|
||||
def ensure_glyph_object(text_properties, glyph_properties):
|
||||
glyph_index = glyph_properties["glyph_index"]
|
||||
# First, let's see if there was ever a glyph object constructed
|
||||
if (
|
||||
glyph_properties.glyph_object is None
|
||||
or not isinstance(glyph_properties.glyph_object, bpy_types.Object)
|
||||
or not is_glyph_object(glyph_properties.glyph_object)
|
||||
):
|
||||
# we do need a text_object though
|
||||
# if there is not, let's give up for this iteration
|
||||
if not isinstance(text_properties.text_object, bpy_types.Object):
|
||||
print(
|
||||
f"{utils.prefix()}::ensure_glyph_object: failed! text object is not an object"
|
||||
)
|
||||
return False
|
||||
|
||||
outer_node = bpy.data.objects.new(f"{glyph_properties.glyph_id}", None)
|
||||
inner_node = bpy.data.objects.new(
|
||||
f"{glyph_properties.glyph_id}_mesh",
|
||||
get_original_glyph(text_properties, glyph_properties).data,
|
||||
)
|
||||
transfer_properties_to_glyph_object(
|
||||
text_properties, glyph_properties, outer_node
|
||||
)
|
||||
|
||||
# Add into the scene.
|
||||
text_properties.text_object.users_collection[0].objects.link(outer_node)
|
||||
text_properties.text_object.users_collection[0].objects.link(inner_node)
|
||||
|
||||
# Parenting is hard.
|
||||
inner_node.parent_type = "OBJECT"
|
||||
inner_node.parent = outer_node
|
||||
inner_node.matrix_parent_inverse = outer_node.matrix_world.inverted()
|
||||
parent_to_curve(outer_node, text_properties.text_object)
|
||||
|
||||
# outer_node["inner_node"] = bpy.types.PointerProperty(inner_node)
|
||||
# for some funny reason we cannot set 'glyph_object' by key, but need to set the attribute
|
||||
glyph_properties.glyph_object = outer_node
|
||||
outer_node[f"{utils.prefix()}_glyph_index"] = glyph_index
|
||||
else:
|
||||
outer_node = glyph_properties.glyph_object
|
||||
outer_node[f"{utils.prefix()}_glyph_index"] = glyph_index
|
||||
|
||||
# we might just want to update the data
|
||||
# imagine a different font, letter or alternate
|
||||
# this way we keep all manual transforms
|
||||
if (
|
||||
glyph_properties.glyph_object[get_key("glyph_id")] != glyph_properties.glyph_id
|
||||
or glyph_properties.glyph_object[get_key("alternate")]
|
||||
!= glyph_properties.alternate
|
||||
or glyph_properties.glyph_object[get_key("font_name")]
|
||||
!= text_properties.font_name
|
||||
or glyph_properties.glyph_object[get_key("face_name")]
|
||||
!= text_properties.face_name
|
||||
):
|
||||
|
||||
inner_node = None
|
||||
old_font_name = glyph_properties.glyph_object[get_key("font_name")]
|
||||
old_face_name = glyph_properties.glyph_object[get_key("face_name")]
|
||||
old_face = Font.get_font_face(old_font_name, old_face_name)
|
||||
face = Font.get_font_face(text_properties.font_name, text_properties.face_name)
|
||||
ratio = old_face.unit_factor / face.unit_factor
|
||||
# try:
|
||||
# inner_node = glyph_properties["inner_node"].original
|
||||
# inner_node.location = inner_node.location * ratio
|
||||
# except KeyError:
|
||||
old_glyph_id = glyph_properties.glyph_object[get_key("glyph_id")]
|
||||
for c in glyph_properties.glyph_object.children:
|
||||
if c.name.startswith(f"{old_glyph_id}_mesh"):
|
||||
inner_node = c
|
||||
inner_node.location = inner_node.location * ratio
|
||||
inner_node.name = f"{glyph_properties.glyph_id}_mesh"
|
||||
# outer_node["inner_node"] = bpy.types.PointerProperty(inner_node)
|
||||
if inner_node is None:
|
||||
print(f"{utils.prefix()}::ensure_glyph_object: failed! no inner_node found")
|
||||
return False
|
||||
inner_node.data = get_original_glyph(text_properties, glyph_properties).data
|
||||
glyph_properties.glyph_object[get_key("glyph_id")] = glyph_properties.glyph_id
|
||||
glyph_properties.glyph_object[get_key("alternate")] = glyph_properties.alternate
|
||||
glyph_properties.glyph_object[get_key("font_name")] = text_properties.font_name
|
||||
glyph_properties.glyph_object[get_key("face_name")] = text_properties.face_name
|
||||
|
||||
glyph_properties.glyph_object.hide_set(True)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def ensure_glyphs(text_properties, predicted_text: str):
|
||||
|
||||
######### REQUIREMENTS
|
||||
|
||||
# turns out this is not a requirement
|
||||
# and can be a case we want to tackle
|
||||
#
|
||||
# if not text_properties.get("glyphs"):
|
||||
# ShowMessageBox(
|
||||
# title="text_properties has no glyphs", message="well, what I said"
|
||||
# )
|
||||
# return False
|
||||
|
||||
######### SETUP
|
||||
|
||||
n_glyphs = len(text_properties.glyphs)
|
||||
n_predicted = len(predicted_text)
|
||||
|
||||
########## ENSURE AMOUNT
|
||||
|
||||
if n_glyphs == n_predicted:
|
||||
# same amount of glyphs
|
||||
# this is the most common case
|
||||
# don't do anything
|
||||
pass
|
||||
|
||||
elif n_glyphs > n_predicted:
|
||||
# more glyphs than predicted
|
||||
# it's a shorter word, or letters were deleted
|
||||
count = n_glyphs - n_predicted
|
||||
for i in range(0, count):
|
||||
reverse_i = n_glyphs - (i + 1)
|
||||
# let's attempt to remove the glyph_object first
|
||||
# so we avoid dangling data
|
||||
if isinstance(
|
||||
text_properties.glyphs[reverse_i].glyph_object, bpy_types.Object
|
||||
):
|
||||
# bam!
|
||||
completely_delete_objects(
|
||||
[text_properties.glyphs[reverse_i].glyph_object]
|
||||
)
|
||||
# else:
|
||||
# # nothing to do, if there is no blender object
|
||||
# # possibly we could do a 'del', but we can also
|
||||
# # just comment out the whole conditional fork
|
||||
# pass
|
||||
|
||||
# now that blender data is gone, we can remove the glyph
|
||||
text_properties.glyphs.remove(reverse_i)
|
||||
|
||||
elif n_glyphs < n_predicted:
|
||||
# less glyphs than predicted
|
||||
# it's a longer word, or letters were added
|
||||
while n_glyphs < n_predicted:
|
||||
glyph_id = predicted_text[n_glyphs]
|
||||
glyph_properties = text_properties.glyphs.add()
|
||||
glyph_properties["glyph_id"] = predicted_text[n_glyphs]
|
||||
glyph_properties["glyph_index"] = n_glyphs
|
||||
glyph_properties["text_id"] = text_properties.text_id
|
||||
glyph_properties["letter_spacing"] = 0
|
||||
n_glyphs += 1
|
||||
|
||||
######### ENSURE VALUES
|
||||
|
||||
for i, glyph_properties in enumerate(text_properties.glyphs):
|
||||
glyph_properties["glyph_index"] = i
|
||||
glyph_properties["text_id"] = text_properties.text_id
|
||||
glyph_properties["glyph_id"] = predicted_text[i]
|
||||
if not ensure_glyph_object(text_properties, glyph_properties):
|
||||
print(f"{utils.prefix()}::ensure_glyphs: could not ensure glyph_object")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# C.scene.abc3d_data.available_texts[0]
|
||||
# import abc3d
|
||||
# abc3d.butils.ensure_glyphs(C.scene.abc3d_data.available_texts[0], "whatever")
|
||||
|
||||
|
||||
def set_text_on_curve(
|
||||
text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, can_regenerate=False
|
||||
):
|
||||
|
@ -1519,27 +1712,8 @@ def set_text_on_curve(
|
|||
|
||||
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 we regenerate.... delete objects
|
||||
if regenerate and text_properties.get("glyphs"):
|
||||
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)
|
||||
predicted_text = predict_actual_text(text_properties)
|
||||
ensure_glyphs(text_properties, predicted_text)
|
||||
|
||||
curve_length = get_curve_length(mom)
|
||||
advance = text_properties.offset
|
||||
|
@ -1549,6 +1723,9 @@ def set_text_on_curve(
|
|||
previous_spline_index = -1
|
||||
|
||||
actual_text = ""
|
||||
# we need to iterate over the original text, as we want commands
|
||||
# however, ideally it could be an array of glyphs, commands and spaces
|
||||
# now we need to handle non existing characters etc everytime in the loop
|
||||
for i, c in enumerate(text_properties.text):
|
||||
face = Font.get_font_face(text_properties.font_name, text_properties.face_name)
|
||||
scalor = face.unit_factor * text_properties.font_size
|
||||
|
@ -1572,90 +1749,31 @@ def set_text_on_curve(
|
|||
|
||||
spline_index = 0
|
||||
|
||||
############### GET GLYPH
|
||||
############### HANDLE SPACES
|
||||
|
||||
glyph_tmp = Font.get_glyph(
|
||||
text_properties.font_name, text_properties.face_name, glyph_id, -1
|
||||
)
|
||||
if glyph_tmp is None:
|
||||
if glyph_id not in predicted_text:
|
||||
space_width = Font.is_space(glyph_id)
|
||||
if space_width:
|
||||
advance = advance + space_width * text_properties.font_size
|
||||
continue
|
||||
|
||||
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
|
||||
continue
|
||||
|
||||
############### GLYPH PROPERTIES
|
||||
|
||||
glyph_properties = (
|
||||
text_properties.glyphs[glyph_index]
|
||||
if not regenerate
|
||||
else text_properties.glyphs.add()
|
||||
)
|
||||
glyph_properties = text_properties.glyphs[glyph_index]
|
||||
# ensure_glyph_object(text_properties, glyph_properties)
|
||||
|
||||
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
|
||||
############### ACTUAL TEXT
|
||||
|
||||
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
|
||||
# outsourced to ensure_glyph_object
|
||||
|
||||
############### TRANSFORMS
|
||||
|
||||
glyph = get_original_glyph(text_properties, glyph_properties)
|
||||
|
||||
# origins could be shifted
|
||||
# so we need to apply a pre_advance
|
||||
glyph_pre_advance, glyph_post_advance = get_glyph_prepost_advances(glyph)
|
||||
|
@ -1683,14 +1801,12 @@ def set_text_on_curve(
|
|||
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
|
||||
previous_glyph_object_rotation_mode = None
|
||||
if glyph_properties.glyph_object.rotation_mode != "QUATERNION":
|
||||
previous_glyph_object_rotation_mode = (
|
||||
glyph_properties.glyph_object.rotation_mode
|
||||
)
|
||||
glyph_properties.glyph_object.rotation_mode = "QUATERNION"
|
||||
|
||||
# get info from bezier
|
||||
location, tangent, spline_index = calc_point_on_bezier_curve(
|
||||
|
@ -1702,7 +1818,9 @@ def set_text_on_curve(
|
|||
is_newline = True
|
||||
|
||||
# position
|
||||
outer_node.location = location + text_properties.translation
|
||||
glyph_properties.glyph_object.location = (
|
||||
location + text_properties.translation
|
||||
)
|
||||
|
||||
# orientation / rotation
|
||||
mask = [0]
|
||||
|
@ -1720,21 +1838,21 @@ def set_text_on_curve(
|
|||
|
||||
q = mathutils.Quaternion()
|
||||
q.rotate(text_properties.orientation)
|
||||
outer_node.rotation_quaternion = (
|
||||
glyph_properties.glyph_object.rotation_quaternion = (
|
||||
motor[0].to_3x3() @ q.to_matrix()
|
||||
).to_quaternion()
|
||||
|
||||
# # NOTE: supercool but out of scope, as we wouldhave to update it everytime the curve object rotates,
|
||||
# # but this would ignore the curve objects orientation:
|
||||
# outer_node.rotation_quaternion = (mom.matrix_world.inverted().to_3x3() @ motor[0].to_3x3() @ q.to_matrix()).to_quaternion()
|
||||
# glyph_properties.glyph_object.rotation_quaternion = (mom.matrix_world.inverted().to_3x3() @ motor[0].to_3x3() @ q.to_matrix()).to_quaternion()
|
||||
|
||||
# # scale
|
||||
outer_node.scale = (scalor, scalor, scalor)
|
||||
glyph_properties.glyph_object.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
|
||||
if previous_glyph_object_rotation_mode:
|
||||
glyph_properties.glyph_object.rotation_mode = (
|
||||
previous_glyph_object_rotation_mode
|
||||
)
|
||||
|
||||
# outer_node.hide_viewport = True
|
||||
|
||||
|
@ -1805,8 +1923,7 @@ def set_text_on_curve(
|
|||
glyph_index += 1
|
||||
previous_spline_index = spline_index
|
||||
|
||||
if regenerate:
|
||||
text_properties["actual_text"] = actual_text
|
||||
text_properties["actual_text"] = actual_text
|
||||
|
||||
return True
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue