font3d_blender_addon/butils.py
themancalledjakob f6b1649c71 smoother install font & depsgraph update
when we set a text, we don't want our manual depsgraph update, because
this would trigger another setting of the text, which would trigger the
manual depsgraph update, which would trigger another setting of the
text, which would trigger the manual depsgraph update, which will
eventually be too much.

So, we ignore the depsgraph update n-times, where "n = n-letters + 1".
worst side effect: might not update the text automatically in edge
cases.
2024-11-15 20:24:06 +01:00

1215 lines
45 KiB
Python

import bpy
import mathutils
import queue
import importlib
import os
import re
from multiprocessing import Process
# import time # for debugging performance
# then import dependencies for our addon
if "Font" in locals():
importlib.reload(Font)
else:
from .common import Font
if "utils" in locals():
importlib.reload(utils)
else:
from .common import utils
execution_queue = queue.Queue()
# This function can safely be called in another thread.
# The function will be executed when the timer runs the next time.
def run_in_main_thread(function):
execution_queue.put(function)
def execute_queued_functions():
while not execution_queue.empty():
function = execution_queue.get()
function()
return 1.0
def apply_all_transforms(obj):
mb = obj.matrix_basis
if hasattr(obj.data, "transform"):
obj.data.transform(mb)
for c in obj.children:
c.matrix_local = mb @ c.matrix_local
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
# Ensure it's a curve object
# TODO: no raising, please
def get_curve_length(curve_obj, resolution = -1):
total_length = 0
curve = curve_obj.data
# Loop through all splines in the curve
for spline in curve.splines:
total_length = total_length + spline.calc_length(resolution=resolution)
return total_length
def get_curve_line_lengths(curve_obj, resolution = -1):
lengths = []
for spline in curve_obj.data.splines:
lengths.append(spline.calc_length(resolution=resolution))
return lengths
def get_next_line_advance(curve_obj, current_advance, previous_glyph_advance, resolution = -1):
curve_line_lengths = get_curve_line_lengths(curve_obj, resolution)
total_length = 0
for cll in curve_line_lengths:
total_length += cll
if current_advance - previous_glyph_advance < total_length:
return total_length
return current_advance
def calc_point_on_bezier(bezier_point_1, bezier_point_2, t):
p1 = bezier_point_1.co
h1 = bezier_point_1.handle_right
p2 = bezier_point_2.co
h2 = bezier_point_2.handle_left
return ((1 - t)**3) * p1 + (3 * t * (1 - t)**2) * h1 + (3 * (t**2) * (1 - t)) * h2 + (t**3) * p2
# same in slightly more lines
# result is equal, performance minimally better perhaps?
# def calc_point_on_bezier(bezier_point_1, bezier_point_2, ratio):
# startPoint = bezier_point_1.co
# controlPoint1 = bezier_point_1.handle_right
# controlPoint2 = bezier_point_2.handle_left
# endPoint = bezier_point_2.co
# remainder = 1 - ratio
# ratioSquared = ratio * ratio
# remainderSquared = remainder * remainder
# startPointMultiplier = remainderSquared * remainder
# controlPoint1Multiplier = remainderSquared * ratio * 3
# controlPoint2Multiplier = ratioSquared * remainder * 3
# endPointMultiplier = ratioSquared * ratio
# return startPoint * startPointMultiplier + controlPoint1 * controlPoint1Multiplier + controlPoint2 * controlPoint2Multiplier + endPoint * endPointMultiplier
def calc_tangent_on_bezier(bezier_point_1, bezier_point_2, t):
p1 = bezier_point_1.co
h1 = bezier_point_1.handle_right
p2 = bezier_point_2.co
h2 = bezier_point_2.handle_left
return (
(-3 * (1 - t)**2) * p1 + (-6 * t * (1 - t) + 3 * (1 - t)**2) * h1 +
(-3 * (t**2) + 6 * t * (1 - t)) * h2 + (3 * t**2) * p2
).normalized()
from math import radians, sqrt, pi, acos
def align_rotations_auto_pivot(mask, input_rotations, vectors, factors, local_main_axis):
output_rotations = [mathutils.Matrix().to_3x3() for _ in range(len(input_rotations))]
for i in mask:
vector = mathutils.Vector(vectors[i]).normalized()
input_rotation = mathutils.Euler(input_rotations[i])
if vector.length < 1e-6:
output_rotations[i] = input_rotation.to_matrix()
continue
old_rotation = input_rotation.to_matrix()
old_axis = (old_rotation @ local_main_axis).normalized()
new_axis = vector
# rotation_axis = (-(old_axis) + new_axis).normalized()
rotation_axis = old_axis.cross(new_axis).normalized()
if rotation_axis.length < 1e-6:
# Vectors are linearly dependent, fallback to another axis
rotation_axis = (old_axis + mathutils.Matrix().to_3x3().col[2]).normalized()
if rotation_axis.length < 1e-6:
# This is now guaranteed to not be zero
rotation_axis = (-(old_axis) + mathutils.Matrix().to_3x3().col[1]).normalized()
# full_angle = radians(sqrt((4 * pow(input_rotation.to_quaternion().dot(mathutils.Quaternion(vectors[i].normalized())), 2) - 3)))
# dot = old_axis.dot(new_axis)
# normalized_diff = (old_axis - new_axis).normalized()
# full_angle = acos(min((old_axis * new_axis + normalized_diff.dot(2)).length, 1))
full_angle = old_axis.angle(new_axis)
angle = factors[i] * full_angle
rotation = mathutils.Quaternion(rotation_axis, angle).to_matrix()
new_rotation_matrix = old_rotation @ rotation
output_rotations[i] = new_rotation_matrix
return [mat.to_4x4() for mat in output_rotations]
def calc_bezier_length(bezier_point_1, bezier_point_2, resolution=20):
step = 1/resolution
previous_p = bezier_point_1.co
length = 0
for i in range(-1, resolution):
t = (i + 1) * step
p = calc_point_on_bezier(bezier_point_1, bezier_point_2, t)
length += (p - previous_p).length
previous_p = p
return length
def calc_point_on_bezier_spline(bezier_spline_obj,
distance,
output_tangent = False,
resolution_factor = 1.0):
# what's the point of just one point
# assert len(bezier_spline_obj.bezier_points) >= 2
# 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")
if output_tangent:
return mathutils.Vector((0,0,0)), mathutils.Vector((1,0,0))
else:
return mathutils.Vector((0,0,0))
if len(bezier_spline_obj.bezier_points) == 1:
p = bezier_spline_obj.bezier_points[0]
travel = (p.handle_left - p.co).normalized() * distance
if output_tangent:
tangent = mathutils.Vector((1,0,0))
return travel, tangent
else:
return travel
if distance <= 0:
p = bezier_spline_obj.bezier_points[0]
travel = (p.co - p.handle_left).normalized() * distance
location = p.co + travel
if output_tangent:
p2 = bezier_spline_obj.bezier_points[1]
tangent = calc_tangent_on_bezier(p, p2, 0)
return location, tangent
else:
return location
beziers = []
lengths = []
total_length = 0
n_bezier_points = len(bezier_spline_obj.bezier_points)
for i in range(0, len(bezier_spline_obj.bezier_points) - 1):
bezier = [ bezier_spline_obj.bezier_points[i],
bezier_spline_obj.bezier_points[i + 1] ]
length = calc_bezier_length(bezier[0],
bezier[1],
int(bezier_spline_obj.resolution_u * resolution_factor))
total_length += length
beziers.append(bezier)
lengths.append(length)
# if total_length > distance:
# break
iterated_distance = 0
for i in range(0, len(beziers)):
if iterated_distance + lengths[i] > distance:
distance_on_bezier = (distance - iterated_distance)
d = distance_on_bezier / lengths[i]
# print(f"i: {i}, d: {d}, distance_on_bezier: {distance_on_bezier}, distance: {distance}")
location = calc_point_on_bezier(beziers[i][0],
beziers[i][1],
d)
if output_tangent:
tangent = calc_tangent_on_bezier(beziers[i][0],
beziers[i][1],
d)
return location, tangent
else:
return location
iterated_distance += lengths[i]
# 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)
location = p.co + travel
if output_tangent:
tangent = calc_tangent_on_bezier(beziers[last_i][0],
p,
1)
return location, tangent
else:
return location
def calc_point_on_bezier_curve(bezier_curve_obj,
distance,
output_tangent = False,
output_spline_index = False,
resolution_factor = 1.0):
curve = bezier_curve_obj.data
# Loop through all splines in the curve
total_length = 0
for i, spline in enumerate(curve.splines):
resolution = int(spline.resolution_u * resolution_factor)
length = spline.calc_length(resolution=resolution)
if total_length + length > distance or i == len(curve.splines) - 1:
if output_spline_index and output_tangent:
# return value from c_p_o_b_s is a tuple
# so we need to append tuple + tuple
return calc_point_on_bezier_spline(spline,
(distance - total_length),
output_tangent,
resolution_factor) + (i,)
if output_spline_index and not output_tangent:
# return value from c_p_o_b_s is a location vector
# so we need to append with a comma
return calc_point_on_bezier_spline(spline,
(distance - total_length),
output_tangent,
resolution_factor), i
else:
return calc_point_on_bezier_spline(spline,
(distance - total_length),
output_tangent,
resolution_factor)
total_length += length
# TODO: can this fail?
# 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)]
def find_objects_by_name(
objects,
equals="",
contains="",
startswith="",
endswith=""):
# handle equals
if equals != "":
index = objects.find(equals)
if index >= 0:
return [objects[index]]
return []
# handle others is more permissive
return [obj for obj in objects if obj.name.startswith(startswith) and obj.name.endswith(endswith) and obj.name.find(contains) >= 0]
def find_objects_by_custom_property(
objects,
property_name="",
property_value=""):
return [obj for obj in objects if property_name in obj and obj[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)
def find_font_object(fontcollection, font_name):
fonts = find_objects_by_custom_property(fontcollection.objects,
"is_font",
True)
for font in fonts:
if font["font_name"] == font_name and font.parent == None:
return font
return None
def find_font_face_object(font_obj, face_name):
faces = find_objects_by_custom_property(font_obj.children,
"is_face",
True)
for face in faces:
if face["face_name"] == face_name:
return face
return None
def move_in_fontcollection(obj, fontcollection, allow_duplicates=False):
# parent nesting structure
# the font object
font_obj = find_font_object(fontcollection,
obj["font_name"])
if font_obj == None:
font_obj = bpy.data.objects.new(obj["font_name"], None)
font_obj.empty_display_type = 'PLAIN_AXES'
fontcollection.objects.link(font_obj)
# ensure custom properties are set
font_obj["font_name"] = obj["font_name"]
font_obj["is_font"] = True
# the face object as a child of font object
face_obj = find_font_face_object(font_obj,
obj["face_name"])
if face_obj == None:
face_obj = bpy.data.objects.new(obj["face_name"], None)
face_obj.empty_display_type = 'PLAIN_AXES'
face_obj["is_face"] = True
fontcollection.objects.link(face_obj)
# ensure custom properties are set
face_obj["face_name"] = obj["face_name"]
face_obj["font_name"] = obj["font_name"]
if face_obj.parent != font_obj:
face_obj.parent = font_obj
# create glyphs if it does not exist
glyphs_objs = find_objects_by_name(face_obj.children, startswith="glyphs")
if len(glyphs_objs) <= 0:
glyphs_obj = bpy.data.objects.new("glyphs", None)
glyphs_obj.empty_display_type = 'PLAIN_AXES'
fontcollection.objects.link(glyphs_obj)
glyphs_obj.parent = face_obj
elif len(glyphs_objs) > 1:
print(f"found more glyphs objects than expected")
# now it must exist
glyphs_obj = find_objects_by_name(face_obj.children, startswith="glyphs")[0]
glyphs_obj["face_name"] = obj["face_name"]
glyphs_obj["font_name"] = obj["font_name"]
def get_hash(o):
return hash(tuple(tuple(v.co) for v in o.data.vertices ))
for other_obj in find_objects_by_custom_property(glyphs_obj.children, "glyph", obj["glyph"]):
if get_hash(other_obj) == get_hash(obj) and not allow_duplicates:
return other_obj
# and now parent it!
if obj.parent != glyphs_obj:
obj.parent = glyphs_obj
for c in obj.users_collection:
c.objects.unlink(obj)
if fontcollection.objects.find(obj.name) < 0:
fontcollection.objects.link(obj)
return obj
def bpy_to_abspath(blender_path):
return os.path.realpath(bpy.path.abspath(blender_path))
def register_font_from_filepath(filepath):
from .bimport import get_font_faces_in_file
availables = get_font_faces_in_file(filepath)
fonts = {}
for a in availables:
font_name = a["font_name"]
face_name = a["face_name"]
glyph = a["glyph"]
if not font_name in fonts:
fonts[font_name] = {}
if not face_name in fonts[font_name]:
fonts[font_name][face_name] = []
fonts[font_name][face_name].append(glyph)
for font_name in fonts:
for face_name in fonts[font_name]:
Font.register_font(font_name,
face_name,
fonts[font_name][face_name],
filepath)
def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""):
if not filepath.endswith(".glb") and not filepath.endswith(".gltf"):
ShowMessageBox(f"Font loading error", 'ERROR', f"Filepath({filepath}) is not a *.glb or *.gltf file")
return False
marker_property = "font_import"
bpy.ops.abc3d.import_font_gltf(filepath=filepath,
glyphs=glyphs,
marker_property=marker_property,
font_name=font_name,
face_name=face_name)
fontcollection = bpy.data.collections.get("ABC3D")
if fontcollection is None:
fontcollection = bpy.data.collections.new("ABC3D")
modified_font_faces = []
all_glyph_os = []
all_objects = []
for o in bpy.context.scene.objects:
if marker_property in o:
if "type" in o and o["type"] == "glyph":
all_glyph_os.append(o)
for o in all_glyph_os:
glyph_id = o["glyph"]
font_name = o["font_name"]
face_name = o["face_name"]
glyph_obj = move_in_fontcollection(
o,
fontcollection)
if glyph_obj == o:
del o[marker_property]
Font.add_glyph(
font_name,
face_name,
glyph_id,
glyph_obj)
for c in o.children:
if is_metrics_object(c):
add_metrics_obj_from_bound_box(glyph_obj,
bound_box_as_array(c.bound_box))
modified_font_faces.append({"font_name": font_name,
"face_name": face_name})
for mff in modified_font_faces:
glyphs = []
face = Font.fonts[mff["font_name"]].faces[mff["face_name"]]
# iterate glyphs
for g in face.glyphs:
# iterate alternates
for glyph in face.glyphs[g]:
glyphs.append(glyph)
if len(glyphs) > 0:
add_default_metrics_to_objects(glyphs)
# calculate unit factor
h = get_glyph_height(glyphs[0])
if h != 0:
face.unit_factor = 1 / h
update_available_fonts()
remove_list = []
for o in bpy.context.scene.collection.all_objects:
if not o.name in fontcollection.all_objects:
if marker_property in o and o[marker_property] == True:
remove_list.append(o)
simply_delete_objects(remove_list)
# completely_delete_objects(remove_list)
def update_available_fonts():
abc3d_data = bpy.context.scene.abc3d_data
for font_name in Font.fonts.keys():
for face_name in Font.fonts[font_name].faces.keys():
found = False
for f in abc3d_data.available_fonts.values():
if font_name == f.font_name and face_name == f.face_name:
found = True
if not found:
f = abc3d_data.available_fonts.add()
f.font_name = font_name
f.face_name = face_name
print(f"{__name__} added {font_name} {face_name}")
# def update_available_texts():
# abc3d_data = bpy.context.scene.abc3d_data
# for o in bpy.context.scene.objects:
# if "linked_textobject" in o.keys():
# i = o["linked_textobject"]
# found = False
# if len(abc3d_data.available_texts) > i:
# if abc3d_data.available_texts[i].glyphs
def getPreferences(context):
preferences = context.preferences
return preferences.addons['abc3d'].preferences
# clear available fonts
def clear_available_fonts():
bpy.context.scene.abc3d_data.available_fonts.clear()
def load_installed_fonts():
preferences = getPreferences(bpy.context)
font_dir = os.path.join(preferences.assets_dir,"fonts")
if os.path.exists(font_dir):
for file in os.listdir(font_dir):
if file.endswith(".glb") or file.endswith(".gltf"):
font_path = os.path.join(font_dir, file)
# ShowMessageBox("Loading Font", "INFO", f"loading font from {font_path}")
# print(f"loading font from {font_path}")
# for f in bpy.context.scene.abc3d_data.available_fonts.values():
# print(f"available font: {f.font_name} {f.face_name}")
register_font_from_filepath(font_path)
load_font_from_filepath(font_path)
def register_installed_fonts():
preferences = getPreferences(bpy.context)
font_dir = os.path.join(preferences.assets_dir,"fonts")
if os.path.exists(font_dir):
for file in os.listdir(font_dir):
if file.endswith(".glb") or file.endswith(".gltf"):
font_path = os.path.join(font_dir, file)
# ShowMessageBox("Loading Font", "INFO", f"loading font from {font_path}")
# print(f"loading font from {font_path}")
# for f in bpy.context.scene.abc3d_data.available_fonts.values():
# print(f"available font: {f.font_name} {f.face_name}")
register_font_from_filepath(font_path)
def ShowMessageBox(title = "Message Box", icon = 'INFO', message=""):
"""Show a simple message box
taken from `Link here <https://blender.stackexchange.com/questions/169844/multi-line-text-box-with-popup-menu>`_
:param title: The title shown in the message top bar
:type title: str
:param icon: The icon to be shown in the message top bar
:type icon: str
:param message: lines of text to display, a.k.a. the message
:type message: str or (str, str, ..)
TIP: Check `Link blender icons <https://docs.blender.org/api/current/bpy_types_enum_items/icon_items.html>`_ for icons you can use
TIP: Or even better, check `Link this addons <https://docs.blender.org/manual/en/latest/addons/development/icon_viewer.html>`_ to also see the icons.
usage:
.. code-block:: python
myLines=("line 1","line 2","line 3")
butils.ShowMessageBox(message=myLines)
or:
.. code-block:: python
butils.ShowMessageBox(title="",message=("AAAAAH","NOOOOO"),icon=)
"""
myLines=message
def draw(self, context):
if isinstance(myLines, str):
self.layout.label(text=myLines)
elif hasattr(myLines, "__iter__"):
for n in myLines:
self.layout.label(text=n)
bpy.context.window_manager.popup_menu(draw, title = title, icon = icon)
def simply_delete_objects(objs):
context_override = bpy.context.copy()
context_override["selected_objects"] = list(objs)
with bpy.context.temp_override(**context_override):
bpy.ops.object.delete()
def completely_delete_objects(objs):
simply_delete_objects(objs)
# remove deleted objects
# this is necessary
for g in objs:
if type(g) != type(None):
try:
bpy.data.objects.remove(g, do_unlink=True)
except ReferenceError as e:
# not important
pass
def is_mesh(o):
return type(o.data) == bpy.types.Mesh
def is_metrics_object(o):
if f"{utils.prefix()}_type" in o:
return o[f"{utils.prefix()}_type"] == 'metrics'
return (re.match(".*_metrics$", o.name) != None or re.match(".*_metrics.[\d]{3}$", o.name) != None) and is_mesh(o)
def is_text_object(o):
if f"{utils.prefix()}_type" in o:
return o[f"{utils.prefix()}_type"] == 'textobject'
for t in bpy.context.scene.abc3d_data.available_texts:
if o == t.text_object:
return True
return False
def is_glyph(o):
if f"{utils.prefix()}_type" in o:
return o[f"{utils.prefix()}_type"] == 'glyph'
try:
return type(o.parent) is not type(None) \
and "glyphs" in o.parent.name \
and is_mesh(o) \
and not is_metrics_object(o)
except ReferenceError as e:
return False
def update_types():
scene = bpy.context.scene
abc3d_data = scene.abc3d_data
for t in abc3d_data.available_texts:
t.text_object[f"{utils.prefix()}_type"] = "textobject"
for g in t.glyphs:
g.glyph_object[f"{utils.prefix()}_type"] = "glyph"
# blender bound_box vertices
#
# 3------7.
# |`. | `. +y
# | `2------6 |
# | | | | |
# 0---|--4. | +--- +x
# `. | `.| `.
# `1------5 `+z
def get_glyph_advance(glyph_obj):
for c in glyph_obj.children:
if is_metrics_object(c):
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_height(glyph_obj):
for c in glyph_obj.children:
if is_metrics_object(c):
return abs(c.bound_box[0][1] - c.bound_box[3][1])
return abs(glyph_obj.bound_box[0][1] - glyph_obj.bound_box[3][1])
def prepare_text(font_name, face_name, text):
loaded, missing, loadable, files = Font.test_glyphs_availability(
font_name,
face_name,
text)
if len(loadable) > 0:
for filepath in files:
load_font_from_filepath(filepath, loadable, font_name, face_name)
return True
def is_bezier(curve):
if len(curve.data.splines) < 1:
return False
return curve.data.splines[0].type == 'BEZIER'
def set_text_on_curve(text_properties, recursive=True):
# starttime = time.perf_counter_ns()
mom = text_properties.text_object
if mom.type != "CURVE":
return False
regenerate = False
glyph_objects = []
for i, g in enumerate(text_properties.glyphs):
glyph_objects.append(g.glyph_object)
# check if perhaps one glyph was deleted
if (type(g.glyph_object) == type(None)
or type(g.glyph_object.parent) == type(None)
or g.glyph_object.parent.users_collection != g.glyph_object.users_collection):
regenerate = True
elif len(text_properties.text) > i and g.glyph_id != text_properties.text[i]:
regenerate = True
elif len(text_properties.text) > i and (g.glyph_object[f"{utils.prefix()}_font_name"] != text_properties.font_name
or g.glyph_object[f"{utils.prefix()}_face_name"] != text_properties.face_name):
regenerate = True
if len(text_properties.text) != len(text_properties.glyphs):
regenerate = True
# blender bug
# https://projects.blender.org/blender/blender/issues/100661
if mom.data.use_path:
regenerate = True
# if we regenerate.... delete objects
if regenerate:
completely_delete_objects(glyph_objects)
text_properties.glyphs.clear()
#TODO: fix selection with context_override
previous_selection = bpy.context.selected_objects
bpy.ops.object.select_all(action='DESELECT')
selected_objects = []
curve_length = get_curve_length(mom)
advance = text_properties.offset
glyph_advance = 0
is_command = False
previous_spline_index = -1
for i, c in enumerate(text_properties.text):
face = Font.fonts[text_properties.font_name].faces[text_properties.face_name]
scalor = face.unit_factor * text_properties.font_size
if c == '\\':
is_command = True
continue
if c == ' ':
advance = advance + scalor
continue
is_newline = False
if is_command:
if c == 'n':
is_newline = True
next_line_advance = get_next_line_advance(mom, advance, glyph_advance)
if advance == next_line_advance:
# self.report({'INFO'}, f"would like to add new line for {text_properties.text} please")
print(f"would like to add new line for {text_properties.text} please")
# TODO: add a new line
advance = next_line_advance + text_properties.offset
continue
is_command = False
glyph_id = c
glyph = Font.get_glyph(text_properties.font_name,
text_properties.face_name,
glyph_id)
if glyph == None:
# self.report({'ERROR'}, f"Glyph not found for {font_name} {face_name} {glyph_id}")
print(f"Glyph not found for {text_properties.font_name} {text_properties.face_name} {glyph_id}")
continue
ob = None
if regenerate:
ob = bpy.data.objects.new(f"{glyph_id}", glyph.data)
ob[f"{utils.prefix()}_type"] = "glyph"
ob[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id
ob[f"{utils.prefix()}_font_name"] = text_properties.font_name
ob[f"{utils.prefix()}_face_name"] = text_properties.face_name
else:
ob = text_properties.glyphs[i].glyph_object
distribution_type = 'CALCULATE' if is_bezier(mom) else 'FOLLOW_PATH'
if distribution_type == 'FOLLOW_PATH':
ob.constraints.new(type='FOLLOW_PATH')
ob.constraints["Follow Path"].target = mom
ob.constraints["Follow Path"].use_fixed_location = True
ob.constraints["Follow Path"].offset_factor = advance / curve_length
ob.constraints["Follow Path"].use_curve_follow = True
ob.constraints["Follow Path"].forward_axis = "FORWARD_X"
ob.constraints["Follow Path"].up_axis = "UP_Y"
elif distribution_type == 'CALCULATE':
location, tangent, spline_index = calc_point_on_bezier_curve(mom, advance, True, True)
if spline_index != previous_spline_index:
is_newline = True
if regenerate:
ob.location = mom.matrix_world @ (location + text_properties.translation)
else:
ob.location = (location + text_properties.translation)
if not text_properties.ignore_orientation:
mask = [0]
input_rotations = [mathutils.Vector((0.0, 0.0, 0.0))]
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 ob.rotation_mode != 'QUATERNION':
ob.rotation_mode = 'QUATERNION'
q = mathutils.Quaternion()
q.rotate(text_properties.orientation)
if regenerate:
ob.rotation_quaternion = (mom.matrix_world @ motor[0] @ q.to_matrix().to_4x4()).to_quaternion()
else:
ob.rotation_quaternion = (motor[0] @ q.to_matrix().to_4x4()).to_quaternion()
else:
q = mathutils.Quaternion()
q.rotate(text_properties.orientation)
ob.rotation_quaternion = q
# ob.rotation_quaternion = (mom.matrix_world @ q.to_matrix().to_4x4()).to_quaternion()
glyph_advance = get_glyph_advance(glyph) * scalor + text_properties.letter_spacing
# now we need to compensate for curvature
# otherwise letters will be closer together the curvier the bezier is
# this could be done more efficiently, but whatever
curve_compensation = 0
if distribution_type == 'CALCULATE' and (not is_newline or spline_index == 0): # TODO: fix newline hack
if text_properties.compensate_curvature and glyph_advance > 0:
previous_location, psi = calc_point_on_bezier_curve(mom, advance, False, True)
new_location, si = calc_point_on_bezier_curve(mom, advance + glyph_advance, False, True)
if psi == si:
while (previous_location - new_location).length > glyph_advance and psi == si:
curve_compensation = curve_compensation - glyph_advance * 0.01
new_location, si = calc_point_on_bezier_curve(mom,
advance + glyph_advance + curve_compensation,
output_tangent=False,
output_spline_index=True)
while (previous_location - new_location).length < glyph_advance and psi == si:
curve_compensation = curve_compensation + glyph_advance * 0.01
new_location, si = calc_point_on_bezier_curve(mom,
advance + glyph_advance + curve_compensation,
output_tangent=False,
output_spline_index=True)
ob.scale = (scalor, scalor, scalor)
advance = advance + glyph_advance + curve_compensation
previous_spline_index = spline_index
if regenerate:
mom.users_collection[0].objects.link(ob)
glyph_data = text_properties.glyphs.add()
glyph_data.glyph_id = glyph_id
glyph_data.glyph_object = ob
glyph_data.letter_spacing = 0
ob.select_set(True)
if regenerate:
mom.select_set(True)
# https://projects.blender.org/blender/blender/issues/100661
mom.data.use_path = False
mom[f"{utils.prefix()}_type"] = "textobject"
mom[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id
mom[f"{utils.prefix()}_font_name"] = text_properties.font_name
mom[f"{utils.prefix()}_face_name"] = text_properties.face_name
mom[f"{utils.prefix()}_font_size"] = text_properties.font_size
mom[f"{utils.prefix()}_letter_spacing"] = text_properties.letter_spacing
mom[f"{utils.prefix()}_orientation"] = text_properties.orientation
mom[f"{utils.prefix()}_translation"] = text_properties.translation
bpy.context.view_layer.objects.active = mom
bpy.ops.object.parent_set(type='OBJECT')
bpy.context.scene.abc3d_data["lock_depsgraph_update_ntimes"] = len(bpy.context.selected_objects)
mom["lock_depsgraph_update_ntimes"] = len(bpy.context.selected_objects)
# endtime = time.perf_counter_ns()
# elapsedtime = endtime - starttime
return True
verification_object = {
f"{utils.prefix()}_type": "textobject",
f"{utils.prefix()}_linked_textobject": 0,
f"{utils.prefix()}_font_name": "font_name",
f"{utils.prefix()}_face_name": "face_name",
f"{utils.prefix()}_font_size": 42,
f"{utils.prefix()}_letter_spacing": 42,
f"{utils.prefix()}_orientation": [0,0,0],
f"{utils.prefix()}_translation": [0,0,0],
}
def verify_text_object(o):
pass
def transfer_text_properties_to_text_object(text_properties, o):
o[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id
o[f"{utils.prefix()}_font_name"] = text_properties.font_name
o[f"{utils.prefix()}_face_name"] = text_properties.face_name
o[f"{utils.prefix()}_font_size"] = text_properties.font_size
o[f"{utils.prefix()}_letter_spacing"] = text_properties.letter_spacing
o[f"{utils.prefix()}_orientation"] = text_properties.orientation
o[f"{utils.prefix()}_translation"] = text_properties.translation
o[f"{utils.prefix()}_text"] = text_properties["text"]
def transfer_text_object_to_text_properties(o, text_properties):
text_properties["text_id"] = o[f"{utils.prefix()}_linked_textobject"]
text_properties["font_name"] = o[f"{utils.prefix()}_font_name"]
text_properties["face_name"] = o[f"{utils.prefix()}_face_name"]
text_properties["font_size"] = o[f"{utils.prefix()}_font_size"]
text_properties["letter_spacing"] = o[f"{utils.prefix()}_letter_spacing"]
text_properties["orientation"] = o[f"{utils.prefix()}_orientation"]
text_properties["translation"] = o[f"{utils.prefix()}_translation"]
text_properties["text"] = o[f"{utils.prefix()}_text"]
# blender bound_box vertices
#
# 3------7.
# |`. | `. +y
# | `2------6 -z |
# | | | | `. |
# 0---|--4. | `+--- +x
# `. | `.|
# `1------5
def add_metrics_obj_from_bound_box(glyph, bound_box=None):
mesh = bpy.data.meshes.new(f"{glyph.name}_metrics") # add the new mesh
obj = bpy.data.objects.new(mesh.name, mesh)
obj["font_name"] = glyph["font_name"]
obj["face_name"] = glyph["face_name"]
obj["glyph"] = glyph["glyph"]
obj[f"{utils.prefix()}_type"] = "metrics"
# remove already existing metrics
remove_metrics = []
for c in glyph.children:
if is_metrics_object(c):
remove_metrics.append(c)
if len(remove_metrics) > 0:
completely_delete_objects(remove_metrics)
col = glyph.users_collection[0]
col.objects.link(obj)
# bpy.context.view_layer.objects.active = obj
obj.parent = glyph
if type(bound_box) == type(None):
bound_box = glyph.bound_box
verts = [bound_box[0],
bound_box[1],
bound_box[2],
bound_box[3],
bound_box[4],
bound_box[5],
bound_box[6],
bound_box[7],
]
edges = [[0,1],[1,2],[2,3],[3,0],
[0,4],[1,5],[2,6],[3,7],
[4,5],[5,6],[6,7],[7,4],
]
faces = []
mesh.from_pydata(verts, edges, faces)
def add_faces_to_metrics(obj):
mesh = bpy.data.meshes.new(f"{obj.name}") # add the new mesh
print(f"add_faces_to_metrics for {obj.name}")
bound_box = bound_box_as_array(obj.bound_box)
verts = [bound_box[0],
bound_box[1],
bound_box[2],
bound_box[3],
bound_box[4],
bound_box[5],
bound_box[6],
bound_box[7],
]
edges = [[0,1],[1,2],[2,3],[3,0],
[0,4],[1,5],[2,6],[3,7],
[4,5],[5,6],[6,7],[7,4],
]
faces = [
[0,1,2], [2,3,0],
[2,6,7], [7,3,2],
[6,5,4], [4,7,6],
[4,5,1], [0,4,1],
[1,5,6], [1,6,2],
[4,0,7], [7,0,3],
]
mesh.from_pydata(verts, edges, faces)
old_mesh = obj.data
obj.data = mesh
bpy.data.meshes.remove(old_mesh)
def remove_faces_from_metrics(obj):
mesh = bpy.data.meshes.new(f"{obj.name}") # add the new mesh
bound_box = bound_box_as_array(obj.bound_box)
verts = [bound_box[0],
bound_box[1],
bound_box[2],
bound_box[3],
bound_box[4],
bound_box[5],
bound_box[6],
bound_box[7],
]
edges = [[0,1],[1,2],[2,3],[3,0],
[0,4],[1,5],[2,6],[3,7],
[4,5],[5,6],[6,7],[7,4],
]
faces = [
]
mesh.from_pydata(verts, edges, faces)
old_mesh = obj.data
obj.data = mesh
bpy.data.meshes.remove(old_mesh)
# duplicate
# def remove_metrics_from_selection():
# for o in bpy.context.selected_objects:
# is_possibly_glyph = is_mesh(o)
# if is_possibly_glyph:
# metrics = []
# for c in o.children:
# if is_metrics_object(c):
# metrics.append(c)
# completely_delete_objects(metrics)
def get_max_bound_box(bb_1, bb_2 = None):
if type(bb_2) == type(None):
bb_2 = bb_1
x_max = max(bb_1[4][0], bb_2[4][0])
x_min = min(bb_1[0][0], bb_2[0][0])
y_max = max(bb_1[3][1], bb_2[3][1])
y_min = min(bb_1[0][1], bb_2[0][1])
z_max = max(bb_1[1][2], bb_2[1][2])
z_min = min(bb_1[0][2], bb_2[0][2])
return [
mathutils.Vector((x_min, y_min, z_min)),
mathutils.Vector((x_min, y_min, z_max)),
mathutils.Vector((x_min, y_max, z_max)),
mathutils.Vector((x_min, y_max, z_min)),
mathutils.Vector((x_max, y_min, z_min)),
mathutils.Vector((x_max, y_min, z_max)),
mathutils.Vector((x_max, y_max, z_max)),
mathutils.Vector((x_max, y_max, z_min)),
]
# blender bound_box vertices
#
# 3------7.
# |`. | `. +y
# | `2------6 |
# | | | | |
# 0---|--4. | +--- +x
# `. | `.| `.
# `1------5 `+z
# why not [ [0] * 3 ] * 8
# https://stackoverflow.com/questions/2397141/how-to-initialize-a-two-dimensional-array-list-of-lists-if-not-using-numpy-in
def bound_box_as_array(bound_box):
array = [ [0] * 3 for i in range(8) ]
for i in range(0, len(bound_box)):
for j in range(0, len(bound_box[i])):
array[i][j] = bound_box[i][j]
return array
##
# @brief get_metrics_bound_box
# generates a metrics bounding box
# where x-width comes from bb
# and y-height + z-depth from bb_uebermetrics
#
# @param bb
# @param bb_uebermetrics
#
# @return metrics
def get_metrics_bound_box(bb, bb_uebermetrics):
metrics = [[0] * 3] * 8
# hurrays
if type(bb_uebermetrics) == bpy.types.bpy_prop_array:
metrics = bound_box_as_array(bb_uebermetrics)
else:
metrics = bb_uebermetrics.copy()
metrics[0][0] = bb[0][0]
metrics[1][0] = bb[1][0]
metrics[2][0] = bb[2][0]
metrics[3][0] = bb[3][0]
metrics[4][0] = bb[4][0]
metrics[5][0] = bb[5][0]
metrics[6][0] = bb[6][0]
metrics[7][0] = bb[7][0]
return metrics
def get_metrics_object(o):
if is_glyph(o):
for c in o.children:
if is_metrics_object(c):
return c
return None
def add_default_metrics_to_objects(objects=None, overwrite_existing=False):
if type(objects) == type(None):
objects=bpy.context.selected_objects
targets = []
reference_bound_box = None
for o in objects:
is_possibly_glyph = is_glyph(o)
if is_possibly_glyph:
metrics = []
for c in o.children:
if is_metrics_object(c):
metrics.append(c)
if len(metrics) == 0:
targets.append(o)
reference_bound_box = get_max_bound_box(o.bound_box, reference_bound_box)
elif len(metrics) >= 0 and overwrite_existing:
completely_delete_objects(metrics)
targets.append(o)
reference_bound_box = get_max_bound_box(o.bound_box, reference_bound_box)
else:
for m in metrics:
reference_bound_box = get_max_bound_box(m.bound_box, reference_bound_box)
for t in targets:
bound_box = get_metrics_bound_box(t.bound_box, reference_bound_box)
add_metrics_obj_from_bound_box(t, bound_box)
def remove_metrics_from_objects(objects=None):
if type(objects) == type(None):
objects=bpy.context.selected_objects
metrics = []
for o in objects:
for c in o.children:
if is_metrics_object(c):
metrics.append(c)
completely_delete_objects(metrics)
def align_metrics_of_objects_to_active_object(objects=None):
if type(objects) == type(None):
objects=bpy.context.selected_objects
if len(objects) == 0:
return "no objects selected"
# define the reference_bound_box
reference_bound_box = None
if type(bpy.context.active_object) == type(None):
return "no active_object, but align_to_active_object is True"
for c in bpy.context.active_object.children:
if is_metrics_object(c):
reference_bound_box = bound_box_as_array(c.bound_box)
break
if type(reference_bound_box) == type(None):
if not is_mesh(bpy.context.active_object):
return "active_object is not a mesh and does not have a metrics child"
reference_bound_box = bound_box_as_array(bpy.context.active_object.bound_box)
# do it
for o in objects:
is_possibly_glyph = is_glyph(o)
if is_possibly_glyph:
metrics = []
for c in o.children:
if is_metrics_object(c):
metrics.append(c)
bb = None
if len(metrics) == 0:
bb = get_metrics_bound_box(o.bound_box,
reference_bound_box)
else:
bb = get_metrics_bound_box(metrics[0].bound_box,
reference_bound_box)
if len(metrics) > 0:
completely_delete_objects(metrics)
add_metrics_obj_from_bound_box(o, bb)
return ""
def align_metrics_of_objects(objects=None):
if type(objects) == type(None):
objects=bpy.context.selected_objects
if len(objects) == 0:
return "no objects selected"
targets = []
reference_bound_box = None
for o in objects:
is_possibly_glyph = is_glyph(o)
if is_possibly_glyph:
metrics = []
for c in o.children:
if is_metrics_object(c):
metrics.append(c)
if len(metrics) == 0:
reference_bound_box = get_max_bound_box(o.bound_box,
reference_bound_box)
elif len(metrics) > 0:
reference_bound_box = get_max_bound_box(metrics[0].bound_box,
reference_bound_box)
targets.append(o)
for t in targets:
metrics = []
for c in t.children:
if is_metrics_object(c):
metrics.append(c)
bound_box = None
if len(metrics) == 0:
bound_box = get_metrics_bound_box(t.bound_box,
reference_bound_box)
else:
bound_box = get_metrics_bound_box(metrics[0].bound_box,
reference_bound_box)
completely_delete_objects(metrics)
add_metrics_obj_from_bound_box(t, bound_box)
return ""