font3d_blender_addon/butils.py

1216 lines
45 KiB
Python
Raw Permalink Normal View History

2024-05-21 18:00:49 +02:00
import bpy
2024-06-27 14:56:43 +02:00
import mathutils
import queue
2024-06-27 14:56:43 +02:00
import importlib
2024-08-04 12:52:37 +02:00
import os
import re
2024-08-14 10:50:57 +02:00
from multiprocessing import Process
2024-08-05 12:58:05 +02:00
# import time # for debugging performance
2024-05-21 18:00:49 +02:00
2024-06-27 14:56:43 +02:00
# then import dependencies for our addon
if "Font" in locals():
importlib.reload(Font)
else:
from .common import Font
2024-07-01 10:25:17 +02:00
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
2024-06-27 14:56:43 +02:00
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()
2024-05-21 18:00:49 +02:00
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
2024-06-27 14:56:43 +02:00
# Ensure it's a curve object
# TODO: no raising, please
2024-07-01 14:39:07 +02:00
def get_curve_length(curve_obj, resolution = -1):
2024-06-27 14:56:43 +02:00
total_length = 0
2024-07-01 10:25:17 +02:00
curve = curve_obj.data
2024-06-27 14:56:43 +02:00
# Loop through all splines in the curve
for spline in curve.splines:
2024-07-01 14:39:07 +02:00
total_length = total_length + spline.calc_length(resolution=resolution)
2024-06-27 14:56:43 +02:00
return total_length
2024-07-02 12:22:24 +02:00
def get_curve_line_lengths(curve_obj, resolution = -1):
2024-08-05 10:19:51 +02:00
lengths = []
2024-07-02 12:22:24 +02:00
for spline in curve_obj.data.splines:
2024-08-05 10:19:51 +02:00
lengths.append(spline.calc_length(resolution=resolution))
return lengths
2024-07-02 12:22:24 +02:00
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
2024-07-01 10:25:17 +02:00
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
2024-08-05 12:54:26 +02:00
# 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
2024-07-01 10:25:17 +02:00
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()
2024-07-01 10:25:17 +02:00
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()
2024-07-01 10:25:17 +02:00
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()
2024-07-01 10:25:17 +02:00
# 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
2024-08-04 15:13:05 +02:00
for i in range(-1, resolution):
2024-07-01 10:25:17 +02:00
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]
2024-07-01 14:39:07 +02:00
# print(f"i: {i}, d: {d}, distance_on_bezier: {distance_on_bezier}, distance: {distance}")
2024-07-01 10:25:17 +02:00
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,
2024-07-01 10:25:17 +02:00
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)
2024-07-01 10:25:17 +02:00
total_length += length
# TODO: can this fail?
2024-06-27 14:56:43 +02:00
# 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]
2024-05-21 18:00:49 +02:00
def turn_collection_hierarchy_into_path(obj):
2024-06-27 14:56:43 +02:00
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
2024-08-04 12:52:37 +02:00
def move_in_fontcollection(obj, fontcollection, allow_duplicates=False):
2024-06-27 14:56:43 +02:00
# 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)
2024-06-27 14:56:43 +02:00
# 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"]
2024-08-04 12:52:37 +02:00
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
2024-06-27 14:56:43 +02:00
# and now parent it!
if obj.parent != glyphs_obj:
obj.parent = glyphs_obj
2024-05-28 14:11:32 +02:00
2024-08-04 12:52:37 +02:00
for c in obj.users_collection:
c.objects.unlink(obj)
if fontcollection.objects.find(obj.name) < 0:
fontcollection.objects.link(obj)
return obj
2024-11-15 20:17:28 +01:00
def bpy_to_abspath(blender_path):
return os.path.realpath(bpy.path.abspath(blender_path))
2024-08-21 14:42:53 +02:00
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=""):
2024-08-04 12:52:37 +02:00
if not filepath.endswith(".glb") and not filepath.endswith(".gltf"):
2024-08-23 16:32:40 +02:00
ShowMessageBox(f"Font loading error", 'ERROR', f"Filepath({filepath}) is not a *.glb or *.gltf file")
2024-08-04 12:52:37 +02:00
return False
2024-08-21 14:42:53 +02:00
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)
2024-08-04 12:52:37 +02:00
2024-08-14 11:26:19 +02:00
fontcollection = bpy.data.collections.get("ABC3D")
2024-08-04 12:52:37 +02:00
if fontcollection is None:
2024-08-14 11:26:19 +02:00
fontcollection = bpy.data.collections.new("ABC3D")
2024-08-04 12:52:37 +02:00
2024-08-21 14:42:53 +02:00
modified_font_faces = []
all_glyph_os = []
2024-08-04 12:52:37 +02:00
all_objects = []
2024-08-21 14:42:53 +02:00
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)
2024-08-21 17:31:19 +02:00
if glyph_obj == o:
del o[marker_property]
2024-08-21 14:42:53 +02:00
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
2024-08-14 10:50:57 +02:00
update_available_fonts()
2024-08-21 14:42:53 +02:00
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)
2024-08-14 10:50:57 +02:00
def update_available_fonts():
2024-08-14 11:26:19 +02:00
abc3d_data = bpy.context.scene.abc3d_data
2024-08-14 10:50:57 +02:00
for font_name in Font.fonts.keys():
for face_name in Font.fonts[font_name].faces.keys():
found = False
2024-08-14 11:26:19 +02:00
for f in abc3d_data.available_fonts.values():
2024-08-14 10:50:57 +02:00
if font_name == f.font_name and face_name == f.face_name:
found = True
if not found:
2024-08-14 11:26:19 +02:00
f = abc3d_data.available_fonts.add()
2024-08-14 10:50:57 +02:00
f.font_name = font_name
f.face_name = face_name
2024-08-14 11:26:19 +02:00
print(f"{__name__} added {font_name} {face_name}")
2024-08-14 10:50:57 +02:00
2024-08-14 11:26:19 +02:00
# 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
2024-08-04 12:52:37 +02:00
def getPreferences(context):
preferences = context.preferences
2024-08-14 11:26:19 +02:00
return preferences.addons['abc3d'].preferences
2024-08-04 12:52:37 +02:00
# clear available fonts
def clear_available_fonts():
2024-08-14 11:26:19 +02:00
bpy.context.scene.abc3d_data.available_fonts.clear()
2024-08-04 12:52:37 +02:00
def load_installed_fonts():
2024-08-14 10:50:57 +02:00
preferences = getPreferences(bpy.context)
2024-08-21 16:06:00 +02:00
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)
2024-08-04 12:52:37 +02:00
2024-08-21 14:42:53 +02:00
def register_installed_fonts():
preferences = getPreferences(bpy.context)
2024-08-21 16:06:00 +02:00
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)
2024-08-21 14:42:53 +02:00
2024-05-28 14:11:32 +02:00
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)
2024-06-27 14:56:43 +02:00
2024-08-21 14:42:53 +02:00
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()
2024-08-21 14:42:53 +02:00
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):
2024-10-31 19:33:49 +01:00
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)
2024-10-31 19:33:49 +01:00
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):
2024-10-31 19:33:49 +01:00
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
2024-10-31 19:33:49 +01:00
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])
2024-08-21 14:42:53 +02:00
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'
2024-08-21 14:42:53 +02:00
def set_text_on_curve(text_properties, recursive=True):
2024-08-05 12:58:05 +02:00
# starttime = time.perf_counter_ns()
2024-06-27 14:56:43 +02:00
mom = text_properties.text_object
if mom.type != "CURVE":
return False
regenerate = False
2024-06-27 14:56:43 +02:00
glyph_objects = []
for i, g in enumerate(text_properties.glyphs):
2024-06-27 14:56:43 +02:00
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
2024-08-14 11:26:19 +02:00
elif len(text_properties.text) > i and g.glyph_id != text_properties.text[i]:
regenerate = True
2024-08-14 14:57:34 +02:00
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):
2024-08-14 10:50:57 +02:00
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()
2024-06-27 14:56:43 +02:00
#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)
2024-08-21 17:31:19 +02:00
advance = text_properties.offset
2024-07-02 12:22:24 +02:00
glyph_advance = 0
is_command = False
previous_spline_index = -1
2024-06-27 14:56:43 +02:00
for i, c in enumerate(text_properties.text):
2024-08-28 17:25:06 +02:00
face = Font.fonts[text_properties.font_name].faces[text_properties.face_name]
scalor = face.unit_factor * text_properties.font_size
2024-07-02 12:22:24 +02:00
if c == '\\':
is_command = True
continue
2024-08-28 17:25:06 +02:00
if c == ' ':
advance = advance + scalor
continue
is_newline = False
2024-07-02 12:22:24 +02:00
if is_command:
if c == 'n':
is_newline = True
2024-07-02 12:22:24 +02:00
next_line_advance = get_next_line_advance(mom, advance, glyph_advance)
if advance == next_line_advance:
2024-08-04 12:52:37 +02:00
# 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
2024-07-02 12:22:24 +02:00
continue
is_command = False
2024-06-27 14:56:43 +02:00
glyph_id = c
2024-06-27 14:56:43 +02:00
glyph = Font.get_glyph(text_properties.font_name,
text_properties.face_name,
2024-06-27 14:56:43 +02:00
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)
2024-08-26 18:48:43 +02:00
ob[f"{utils.prefix()}_type"] = "glyph"
2024-08-14 14:57:34 +02:00
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:
2024-08-14 11:26:19 +02:00
ob = text_properties.glyphs[i].glyph_object
2024-06-27 14:56:43 +02:00
distribution_type = 'CALCULATE' if is_bezier(mom) else 'FOLLOW_PATH'
2024-07-01 14:39:07 +02:00
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()
2024-06-27 14:56:43 +02:00
glyph_advance = get_glyph_advance(glyph) * scalor + text_properties.letter_spacing
2024-06-27 14:56:43 +02:00
2024-08-05 12:56:30 +02:00
# 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
2024-08-28 17:25:06 +02:00
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)
2024-08-05 12:56:30 +02:00
2024-06-27 14:56:43 +02:00
ob.scale = (scalor, scalor, scalor)
2024-08-05 12:56:30 +02:00
advance = advance + glyph_advance + curve_compensation
previous_spline_index = spline_index
2024-06-27 14:56:43 +02:00
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
2024-08-26 18:48:43 +02:00
mom[f"{utils.prefix()}_type"] = "textobject"
2024-08-14 14:57:34 +02:00
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)
2024-06-27 14:56:43 +02:00
2024-08-05 12:58:05 +02:00
# endtime = time.perf_counter_ns()
# elapsedtime = endtime - starttime
2024-06-27 14:56:43 +02:00
return True
2024-08-26 18:48:43 +02:00
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"]
2024-08-26 20:46:14 +02:00
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)
2024-08-07 13:11:05 +02:00
# 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
2024-08-21 14:42:53 +02:00
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:
2024-08-07 13:11:05 +02:00
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)
2024-08-07 13:11:05 +02:00
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:
2024-08-07 13:11:05 +02:00
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)
2024-08-07 13:11:05 +02:00
completely_delete_objects(metrics)
add_metrics_obj_from_bound_box(t, bound_box)
return ""
2024-08-07 13:11:51 +02:00