font3d_blender_addon/butils.py
2024-08-21 16:51:00 +02:00

1108 lines
40 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,
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:
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 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"{bl_info['name']} 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 = []
remove_list = []
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)
else:
remove_list.append(o)
for o in all_glyph_os:
glyph_id = o["glyph"]
font_name = o["font_name"]
face_name = o["face_name"]
del o[marker_property]
glyph_obj = move_in_fontcollection(
o,
fontcollection)
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})
if glyph_obj != o:
remove_list.append(o)
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")
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")
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):
return (re.match(".*_metrics$", o.name) != None or re.match(".*_metrics.[\d]{3}$", o.name) != None) and is_mesh(o)
def is_glyph(o):
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
# 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
# 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 = 0
glyph_advance = 0
is_command = False
for i, c in enumerate(text_properties.text):
if c == '\\':
is_command = True
continue
if is_command:
if c == 'n':
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
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()}_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 = calc_point_on_bezier_curve(mom, advance, True)
ob.location = mom.matrix_world @ (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)
ob.rotation_quaternion = (mom.matrix_world @ 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()
face = Font.fonts[text_properties.font_name].faces[text_properties.face_name]
scalor = face.unit_factor * text_properties.font_size
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':
if text_properties.compensate_curvature and glyph_advance > 0:
previous_location = calc_point_on_bezier_curve(mom, advance, False)
new_location = calc_point_on_bezier_curve(mom, advance + glyph_advance, False)
while (previous_location - new_location).length > glyph_advance:
curve_compensation = curve_compensation - glyph_advance * 0.01
new_location = calc_point_on_bezier_curve(mom, advance + glyph_advance + curve_compensation, False)
while (previous_location - new_location).length < glyph_advance:
curve_compensation = curve_compensation + glyph_advance * 0.01
new_location = calc_point_on_bezier_curve(mom, advance + glyph_advance + curve_compensation, False)
ob.scale = (scalor, scalor, scalor)
advance = advance + glyph_advance + curve_compensation
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)
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')
# endtime = time.perf_counter_ns()
# elapsedtime = endtime - starttime
return True
# 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["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 ""