Compare commits
7 commits
59edb2e786
...
a5602a6095
Author | SHA1 | Date | |
---|---|---|---|
a5602a6095 | |||
cd99362bb1 | |||
f02f8fc2f0 | |||
e95266afc9 | |||
58e0df3427 | |||
7de8fcc5d1 | |||
14d1b7a160 |
5 changed files with 517 additions and 166 deletions
|
@ -5,7 +5,7 @@
|
|||
/ ___ \| |_) | |___ ___) | |_| |
|
||||
/_/ \_\____/ \____|____/|____/
|
||||
```
|
||||
v0.0.11
|
||||
v0.0.12
|
||||
|
||||
Convenience addon to work with 3D typography in Blender and Cinema4D.
|
||||
|
||||
|
|
145
__init__.py
145
__init__.py
|
@ -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
511
butils.py
|
@ -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
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ def get_version_minor():
|
|||
|
||||
|
||||
def get_version_patch():
|
||||
return 11
|
||||
return 12
|
||||
|
||||
|
||||
def get_version_string():
|
||||
|
|
Loading…
Add table
Reference in a new issue