Compare commits

...

7 commits

5 changed files with 517 additions and 166 deletions

View file

@ -5,7 +5,7 @@
/ ___ \| |_) | |___ ___) | |_| |
/_/ \_\____/ \____|____/|____/
```
v0.0.11
v0.0.12
Convenience addon to work with 3D typography in Blender and Cinema4D.

View file

@ -16,7 +16,7 @@ from .common import Font, utils
bl_info = {
"name": "ABC3D",
"author": "Jakob Schlötter, Studio Pointer*",
"version": (0, 0, 11),
"version": (0, 0, 12),
"blender": (4, 1, 0),
"location": "VIEW3D",
"description": "Convenience addon for 3D fonts",
@ -143,13 +143,40 @@ class ABC3D_glyph_properties(bpy.types.PropertyGroup):
t = butils.get_text_properties(self.text_id)
if t is not None:
butils.set_text_on_curve(t)
return None
def alternate_get_callback(self):
return self["alternate"] if "alternate" in self else 0
def alternate_set_callback(self, value):
min_value = 0
new_value = max(value, min_value)
if self.text_id >= 0:
text_properties = butils.get_text_properties(self.text_id)
max_value = (
len(
Font.get_glyphs(
text_properties.font_name,
text_properties.face_name,
self.glyph_id,
)
)
- 1
)
new_value = min(new_value, max_value)
self["alternate"] = new_value
return None
glyph_id: bpy.props.StringProperty(maxlen=1)
text_id: bpy.props.IntProperty(
default=-1,
)
alternate: bpy.props.IntProperty(
default=-1,
default=0, # also change in alternate_get_callback
get=alternate_get_callback,
set=alternate_set_callback,
update=update_callback,
)
glyph_object: bpy.props.PointerProperty(type=bpy.types.Object)
@ -267,6 +294,7 @@ class ABC3D_data(bpy.types.PropertyGroup):
available_texts: bpy.props.CollectionProperty(
type=ABC3D_text_properties, name="Available texts"
)
texts: bpy.props.CollectionProperty(type=ABC3D_text_properties, name="texts")
def active_text_index_update(self, context):
lock_depsgraph_updates()
@ -660,42 +688,34 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel):
# and bpy.context.object.select_get():
a_o = bpy.context.active_object
if a_o is not None:
if f"{utils.prefix()}_text_id" in a_o:
text_index = a_o[f"{utils.prefix()}_text_id"]
return bpy.context.scene.abc3d_data.available_texts[text_index]
elif a_o.parent is not None and f"{utils.prefix()}_text_id" in a_o.parent:
text_index = a_o.parent[f"{utils.prefix()}_text_id"]
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
# if f"{utils.prefix()}_text_id" in a_o:
# text_id = a_o[f"{utils.prefix()}_text_id"]
# return butils.get_text_properties(text_id)
# # elif a_o.parent is not None and f"{utils.prefix()}_text_id" in a_o.parent:
# # text_id = a_o.parent[f"{utils.prefix()}_text_id"]
# # return butils.get_text_properties(text_id)
# 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
def get_active_glyph_properties(self):
def get_active_glyph_properties(self, text_properties):
if text_properties is None:
return None
a_o = bpy.context.active_object
if a_o is not None:
if (
f"{utils.prefix()}_text_id" in a_o
and f"{utils.prefix()}_glyph_index" in a_o
):
text_index = a_o[f"{utils.prefix()}_text_id"]
if f"{utils.prefix()}_glyph_index" in a_o:
glyph_index = a_o[f"{utils.prefix()}_glyph_index"]
return bpy.context.scene.abc3d_data.available_texts[text_index].glyphs[
glyph_index
]
if len(text_properties.glyphs) <= glyph_index:
return None
return text_properties.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
for g in text_properties.glyphs:
if butils.is_or_has_parent(a_o, g.glyph_object, max_depth=4):
return g
return None
@classmethod
@ -709,7 +729,7 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel):
layout = self.layout
props = self.get_active_text_properties()
glyph_props = self.get_active_glyph_properties()
glyph_props = self.get_active_glyph_properties(props)
if props is None or props.text_object is None:
# this should not happen
@ -721,6 +741,22 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel):
layout.label(text="props.text_object is none")
return
# TODO: put this at a better place
# here we set the font if it is not correct
# this is a fix for a UI glitch, perhaps it could be fixed
# rather where it is not set properly
# if (
# butils.get_key("font_name") in props.text_object
# and butils.get_key("face_name") in props.text_object
# ):
# font = f"{props.text_object[butils.get_key('font_name')]} {props.text_object[butils.get_key('face_name')]}"
# if font != props.font:
#
# def setfont():
# props.font = font
#
# butils.run_in_main_thread(setfont)
#
layout.label(text=f"Mom: {props.text_object.name}")
layout.row().prop(props, "font")
layout.row().prop(props, "text")
@ -737,8 +773,26 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel):
if glyph_props is None:
return
box = layout.box()
box.label(text=f"{glyph_props.glyph_id}")
box.label(text=f"selected character: {glyph_props.glyph_id}")
box.row().prop(glyph_props, "letter_spacing")
# if True:
# font_name = props.font_name
# face_name = props.face_name
# glyph_id = glyph_props.glyph_id
# glyphs_n = len(Font.get_glyphs(font_name, face_name, glyph_id))
# glyph_props.alternate.hard_min = -1
# glyph_props.alternate.hard_max = glyphs_n - 1
n_alternates = len(
Font.get_glyphs(
props.font_name,
props.face_name,
glyph_props.glyph_id,
)
)
if n_alternates > 1:
box.row().prop(glyph_props, "alternate", text=f"alternate ({n_alternates})")
# if glyph_props.glyph_object.preview is not None:
# box.row().template_preview(glyph_props.glyph_object.preview.icon_id)
class ABC3D_OT_RefreshAvailableFonts(bpy.types.Operator):
@ -1594,9 +1648,10 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator):
def do_autodetect_names(self, name: str):
ifxsplit = name.split("_")
if len(ifxsplit) < 4:
print(f"name could not be autodetected {name}")
print("split:")
print(ifxsplit)
print(
f"{utils.prefix()}::CreateFontFromObjects: name could not be autodetected {name}"
)
print(f"{utils.prefix()}::CreateFontFromObjects: split: {ifxsplit=}")
return self.font_name, self.face_name
detected_font_name = f"{ifxsplit[1]}_{ifxsplit[2]}"
detected_face_name = ifxsplit[3]
@ -1671,9 +1726,11 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator):
row.label(text=f"{k}{Font.known_misspellings[k]}{character}")
def execute(self, context):
print(f"executing {self.bl_idname}")
print(f"{utils.prefix()}::CreateFontFromObjects: executing {self.bl_idname}")
if len(context.selected_objects) == 0:
print(f"cancelled {self.bl_idname} - no objects selected")
print(
f"{utils.prefix()}::CreateFontFromObjects: cancelled {self.bl_idname} - no objects selected"
)
return {"CANCELLED"}
global shared
scene = bpy.context.scene
@ -1690,7 +1747,7 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator):
currentObjects = []
for o in context.selected_objects:
if o.name not in currentObjects:
print(f"processing {o.name}")
print(f"{utils.prefix()}::CreateFontFromObjects: processing {o.name}")
process_object = True
if self.autodetect_names:
font_name, face_name = self.do_autodetect_names(o.name)
@ -1727,7 +1784,9 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator):
f.face_name = face_name
else:
print(f"import warning: did not understand glyph {name}")
print(
f"{utils.prefix()}::CreateFontFromObjects: import warning: did not understand glyph {name}"
)
self.report({"INFO"}, f"did not understand glyph {name}")
return {"FINISHED"}
@ -1977,6 +2036,10 @@ def on_depsgraph_update(scene, depsgraph):
if text_properties is not None:
if text_properties.text_object == u.id.original:
# nothing to do
try:
butils.set_text_on_curve(text_properties)
except:
pass
pass
elif butils.is_text_object_legit(u.id.original):
# must be duplicate

511
butils.py
View file

@ -217,6 +217,132 @@ def calc_bezier_length(bezier_point_1, bezier_point_2, resolution=20):
return length
def get_hook_modifiers(blender_object: bpy.types.Object):
return [m for m in blender_object.modifiers if m.type == "HOOK"]
class HookBezierSplinePoint:
def __init__(
self,
handle_left: mathutils.Vector,
co: mathutils.Vector,
handle_right: mathutils.Vector,
):
self.handle_left: mathutils.Vector = mathutils.Vector(handle_left)
self.co: mathutils.Vector = mathutils.Vector(co)
self.handle_right: mathutils.Vector = mathutils.Vector(handle_right)
class HookBezierSpline:
def __init__(
self,
n: int,
use_cyclic_u: bool,
resolution_u: int,
):
self.bezier_points = [HookBezierSplinePoint] * n
self.use_cyclic_u: int = use_cyclic_u
self.resolution_u: int = resolution_u
self.beziers: []
self.lengths: [float]
self.total_length: float
def calc_length(self, resolution) -> float:
# ignore resolution when accessing length to imitate blender function
return self.total_length
class HookBezierData:
def __init__(self, n):
self.splines = [HookBezierSpline] * n
class HookBezierCurve:
def __init__(self, blender_curve: bpy.types.Object, resolution_factor=1.0):
self.data = HookBezierData(len(blender_curve.data.splines))
i = 0
hooks = get_hook_modifiers(blender_curve)
for si, blender_spline in enumerate(blender_curve.data.splines):
self.data.splines[si] = HookBezierSpline(
len(blender_spline.bezier_points),
blender_spline.use_cyclic_u,
blender_spline.resolution_u,
)
for pi, blender_bezier_point in enumerate(blender_spline.bezier_points):
self.data.splines[si].bezier_points[pi] = HookBezierSplinePoint(
blender_bezier_point.handle_left,
blender_bezier_point.co,
blender_bezier_point.handle_right,
)
for hook in hooks:
hook_co = False
hook_handle_left = False
hook_handle_right = False
for vi in hook.vertex_indices:
if vi == i * 3:
hook_handle_left = True
elif vi == i * 3 + 1:
hook_co = True
elif vi == i * 3 + 2:
hook_handle_right = True
if hook_co:
location = (
blender_curve.matrix_world.inverted()
@ hook.object.matrix_world.translation
)
self.data.splines[si].bezier_points[pi].co = (
self.data.splines[si]
.bezier_points[pi]
.co.lerp(location, hook.strength)
)
if hook_handle_left:
location_left = location + (
self.data.splines[si].bezier_points[pi].co
- self.data.splines[si].bezier_points[pi].handle_left
)
self.data.splines[si].bezier_points[pi].handle_left = (
self.data.splines[si]
.bezier_points[pi]
.co.lerp(location_left, hook.strength)
)
if hook_handle_right:
location_right = location + (
self.data.splines[si].bezier_points[pi].co
- self.data.splines[si].bezier_points[pi].handle_right
)
self.data.splines[si].bezier_points[pi].handle_right = (
self.data.splines[si]
.bezier_points[pi]
.co.lerp(location, hook.strength)
)
elif hook_handle_left:
location = (
blender_curve.matrix_world.inverted()
@ hook.object.matrix_world.translation
)
self.data.splines[si].bezier_points[pi].handle_left = (
self.data.splines[si]
.bezier_points[pi]
.handle_left.lerp(location, hook.strength)
)
elif hook_handle_right:
location = (
blender_curve.matrix_world.inverted()
@ hook.object.matrix_world.translation
)
self.data.splines[si].bezier_points[pi].handle_right = (
self.data.splines[si]
.bezier_points[pi]
.handle_right.lerp(location, hook.strength)
)
i += 1
(
self.data.splines[si].beziers,
self.data.splines[si].lengths,
self.data.splines[si].total_length,
) = get_real_beziers_and_lengths(self.data.splines[si], resolution_factor)
def get_real_beziers_and_lengths(bezier_spline_obj, resolution_factor):
beziers = []
lengths = []
@ -277,8 +403,14 @@ 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)
if not isinstance(bezier_spline_obj, HookBezierSpline)
else (
bezier_spline_obj.beziers,
bezier_spline_obj.lengths,
bezier_spline_obj.total_length,
)
)
travel_point = calc_point_on_bezier(beziers[0][1], beziers[0][0], 0.001)
travel = travel_point.normalized() * distance
@ -291,8 +423,14 @@ 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)
if not isinstance(bezier_spline_obj, HookBezierSpline)
else (
bezier_spline_obj.beziers,
bezier_spline_obj.lengths,
bezier_spline_obj.total_length,
)
)
iterated_distance = 0
@ -336,7 +474,9 @@ def calc_point_on_bezier_curve(
output_spline_index=False,
resolution_factor=1.0,
):
curve = bezier_curve_obj.data
bezier_curve = HookBezierCurve(bezier_curve_obj)
curve = bezier_curve.data
# curve = bezier_curve_obj.data
# Loop through all splines in the curve
total_length = 0
@ -663,7 +803,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 +1151,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 +1326,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,8 +1442,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["glyph_object"] = glyph_object
inner_node = None
for c in glyph_object.children:
if c.name.startswith(f"{glyph_id}_mesh"):
@ -1309,6 +1451,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
@ -1419,10 +1564,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
@ -1474,6 +1630,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()
@ -1489,6 +1646,187 @@ 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")
transfer_text_properties_to_text_object(
text_properties, text_properties.text_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
):
@ -1509,7 +1847,11 @@ def set_text_on_curve(
# global lock_depsgraph_update_n_times
# starttime = time.perf_counter_ns()
if text_properties is None:
return False
mom = text_properties.text_object
if mom is None:
return False
if mom.type != "CURVE":
return False
if len(mom.users_collection) < 1:
@ -1517,27 +1859,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
@ -1547,6 +1870,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
@ -1570,90 +1896,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)
@ -1681,26 +1948,29 @@ 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(
mom, applied_advance, True, True
)
# location, tangent, spline_index = calc_point_on_bezier_curve(
# mom_hooked, 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
glyph_properties.glyph_object.location = (
location + text_properties.translation
)
# orientation / rotation
mask = [0]
@ -1718,21 +1988,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
@ -1803,8 +2073,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

View file

@ -266,7 +266,13 @@ def get_glyphs(font_name, face_name, glyph_id):
return glyphs_for_id
def get_glyph(font_name, face_name, glyph_id, alternate=0):
def get_glyph(
font_name: str,
face_name: str,
glyph_id: str,
alternate: int = 0,
alternate_tolerant: bool = True,
):
"""add_glyph adds a glyph to a FontFace
it creates the :class:`Font` and :class:`FontFace` if it does not exist yet
@ -276,6 +282,10 @@ def get_glyph(font_name, face_name, glyph_id, alternate=0):
:type face_name: str
:param glyph_id: The ``glyph_id`` from the glyph you want
:type glyph_id: str
:param alternate: The ``alternate`` from the glyph you want
:type alternate: int
:param alternate_tolerant: Fetch an existing alternate if requested is out of bounds
:type glyph_id: bool
...
:return: returns the glyph object, or ``None`` if it does not exist
:rtype: `Object`
@ -283,12 +293,21 @@ def get_glyph(font_name, face_name, glyph_id, alternate=0):
glyphs = get_glyphs(font_name, face_name, glyph_id)
if len(glyphs) <= alternate or len(glyphs) == 0:
if len(glyphs) == 0:
print(
f"ABC3D::get_glyph: font({font_name}) face({face_name}) glyph({glyph_id})[{alternate}] not found"
)
return None
if len(glyphs) <= alternate:
if alternate_tolerant:
alternate = 0
else:
print(
f"ABC3D::get_glyph: font({font_name}) face({face_name}) glyph({glyph_id})[{alternate}] not found"
)
return None
return glyphs[alternate]

View file

@ -8,7 +8,7 @@ def get_version_minor():
def get_version_patch():
return 11
return 12
def get_version_string():