1128 lines
41 KiB
Python
1128 lines
41 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 = f"{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 = f"{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 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:
|
|
# loaded, missing, maybe, files = Font.test_glyphs_availability(
|
|
# text_properties.font_name,
|
|
# text_properties.face_name,
|
|
# text_properties.text)
|
|
# if len(maybe) > 0 and recursive:
|
|
# print(f"doing the thing {len(files)} times")
|
|
# for filepath in files:
|
|
# def loader():
|
|
# set_text_on_curve(text_properties, False)
|
|
# print(f"loading font from filepath {filepath} {maybe}")
|
|
# load_font_from_filepath(filepath, maybe)
|
|
# print(f"font: {text_properties.font_name} face: {text_properties.face_name}")
|
|
# print("text",text_properties.text)
|
|
# text_properties.font_size = text_properties.font_size
|
|
# # run_in_main_thread(loader)
|
|
# return
|
|
completely_delete_objects(glyph_objects)
|
|
# context_override = bpy.context.copy()
|
|
# context_override["selected_objects"] = list(glyph_objects)
|
|
# with bpy.context.temp_override(**context_override):
|
|
# bpy.ops.object.delete()
|
|
|
|
# # remove deleted objects
|
|
# # this is necessary
|
|
# for g in glyph_objects:
|
|
# if type(g) != type(None):
|
|
# bpy.data.objects.remove(g, do_unlink=True)
|
|
|
|
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 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 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 ""
|
|
|