5c4ee030d3
add functions for metrics saving and loading we need to add and remove faces. if they don't have faces, blender gltf loader will ignore the meshes. but we want them in the end only with vertices and edges.
990 lines
35 KiB
Python
990 lines
35 KiB
Python
import bpy
|
|
import mathutils
|
|
import queue
|
|
import importlib
|
|
import os
|
|
import re
|
|
# 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 load_font_from_filepath(filepath):
|
|
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
|
|
|
|
font3d_data = bpy.context.scene.font3d_data
|
|
allObjectsBefore = []
|
|
for ob in bpy.data.objects:
|
|
allObjectsBefore.append(ob.name)
|
|
|
|
bpy.ops.import_scene.gltf(filepath=filepath)
|
|
|
|
fontcollection = bpy.data.collections.get("Font3D")
|
|
if fontcollection is None:
|
|
fontcollection = bpy.data.collections.new("Font3D")
|
|
|
|
remove_list = []
|
|
all_objects = []
|
|
for o in bpy.data.objects:
|
|
all_objects.append(o)
|
|
for o in all_objects:
|
|
if o.name not in allObjectsBefore:
|
|
# must be new
|
|
if ("glyph" in o.keys()
|
|
and "face_name" in o.keys()
|
|
and "font_name" in o.keys()
|
|
and not ("type" in o.keys() and o["type"] == "metrics")
|
|
and not is_metrics_object(o)
|
|
):
|
|
glyph_id = o["glyph"]
|
|
font_name = o["font_name"]
|
|
face_name = o["face_name"]
|
|
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))
|
|
if glyph_obj != o:
|
|
remove_list.append(o)
|
|
|
|
found = False
|
|
for f in font3d_data.available_fonts.values():
|
|
if f.font_name == font_name:
|
|
found = True
|
|
break
|
|
if not found:
|
|
f = font3d_data.available_fonts.add()
|
|
f.font_name = font_name
|
|
print(f"{__name__} added {font_name}")
|
|
else:
|
|
remove_list.append(o)
|
|
for o in remove_list:
|
|
bpy.data.objects.remove(o, do_unlink=True)
|
|
print(f"{__name__}: loaded fonts")
|
|
|
|
def getPreferences(context):
|
|
preferences = context.preferences
|
|
return preferences.addons['font3d'].preferences
|
|
|
|
# clear available fonts
|
|
def clear_available_fonts():
|
|
bpy.context.scene.font3d_data.available_fonts.clear()
|
|
|
|
def load_available_fonts():
|
|
preferences = getPreferences(bpy.context)
|
|
|
|
currentObjects = []
|
|
for ob in bpy.data.objects:
|
|
currentObjects.append(ob.name)
|
|
|
|
print(f"assets folder: {preferences.assets_dir}")
|
|
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)
|
|
load_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 completely_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()
|
|
|
|
# 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("[\w]*_metrics$", o.name) != None or re.match("[\w]*_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 set_text_on_curve(text_properties):
|
|
# 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
|
|
if len(text_properties.text) > i and g.glyph_id != text_properties.text[i]:
|
|
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)
|
|
# 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.font_face,
|
|
glyph_id)
|
|
|
|
if glyph == None:
|
|
# self.report({'ERROR'}, f"Glyph not found for {font_name} {font_face} {glyph_id}")
|
|
print(f"Glyph not found for {text_properties.font_name} {text_properties.font_face} {glyph_id}")
|
|
continue
|
|
|
|
ob = None
|
|
if regenerate:
|
|
ob = bpy.data.objects.new(f"{glyph_id}", glyph.data)
|
|
ob['linked_textobject'] = text_properties.text_id
|
|
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()
|
|
|
|
scalor = 0.001 * 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:
|
|
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)
|
|
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"
|
|
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
|
|
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 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 ""
|
|
|