use class for glyph availability use isinstance instead of type better user experience when export directory does not exist
1979 lines
66 KiB
Python
1979 lines
66 KiB
Python
import importlib
|
|
import os
|
|
import queue
|
|
import re
|
|
|
|
import bpy
|
|
import mathutils
|
|
|
|
# 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()
|
|
lock_depsgraph_update_n_times = -1
|
|
|
|
|
|
# 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
|
|
|
|
def get_key(key):
|
|
return f"{utils.prefix()}_{key}"
|
|
|
|
|
|
# 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()
|
|
|
|
|
|
|
|
|
|
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)
|
|
real_n_bezier_points = len(bezier_spline_obj.bezier_points)
|
|
if bezier_spline_obj.use_cyclic_u:
|
|
n_bezier_points += 1
|
|
for i in range(0, n_bezier_points - 1):
|
|
i_a = i % (n_bezier_points - 1)
|
|
i_b = (i_a + 1) % real_n_bezier_points
|
|
bezier = [
|
|
bezier_spline_obj.bezier_points[i_a],
|
|
bezier_spline_obj.bezier_points[i_b],
|
|
]
|
|
length = calc_bezier_length(
|
|
bezier[0],
|
|
bezier[1],
|
|
int(bezier_spline_obj.resolution_u * resolution_factor),
|
|
)
|
|
total_length += length
|
|
beziers.append(bezier)
|
|
lengths.append(length)
|
|
# if total_length > distance:
|
|
# break
|
|
|
|
iterated_distance = 0
|
|
for i in range(0, len(beziers)):
|
|
if iterated_distance + lengths[i] > distance:
|
|
distance_on_bezier = distance - iterated_distance
|
|
d = distance_on_bezier / lengths[i]
|
|
# print(f"i: {i}, d: {d}, distance_on_bezier: {distance_on_bezier}, distance: {distance}")
|
|
location = calc_point_on_bezier(beziers[i][0], beziers[i][1], d)
|
|
if output_tangent:
|
|
tangent = calc_tangent_on_bezier(beziers[i][0], beziers[i][1], d)
|
|
return location, tangent
|
|
else:
|
|
return location
|
|
iterated_distance += lengths[i]
|
|
# if we are here, the point is outside the spline
|
|
last_i = len(beziers) - 1
|
|
p = beziers[last_i][1]
|
|
travel = (p.handle_right - p.co).normalized() * (distance - total_length)
|
|
location = p.co + travel
|
|
if output_tangent:
|
|
tangent = calc_tangent_on_bezier(beziers[last_i][0], p, 1)
|
|
return location, tangent
|
|
else:
|
|
return location
|
|
|
|
|
|
def calc_point_on_bezier_curve(
|
|
bezier_curve_obj,
|
|
distance,
|
|
output_tangent=False,
|
|
output_spline_index=False,
|
|
resolution_factor=1.0,
|
|
):
|
|
curve = bezier_curve_obj.data
|
|
|
|
# Loop through all splines in the curve
|
|
total_length = 0
|
|
for i, spline in enumerate(curve.splines):
|
|
resolution = int(spline.resolution_u * resolution_factor)
|
|
length = spline.calc_length(resolution=resolution)
|
|
if total_length + length > distance or i == len(curve.splines) - 1:
|
|
if output_spline_index and output_tangent:
|
|
# return value from c_p_o_b_s is a tuple
|
|
# so we need to append tuple + tuple
|
|
return calc_point_on_bezier_spline(
|
|
spline, (distance - total_length), output_tangent, resolution_factor
|
|
) + (i,)
|
|
if output_spline_index and not output_tangent:
|
|
# return value from c_p_o_b_s is a location vector
|
|
# so we need to append with a comma
|
|
return (
|
|
calc_point_on_bezier_spline(
|
|
spline,
|
|
(distance - total_length),
|
|
output_tangent,
|
|
resolution_factor,
|
|
),
|
|
i,
|
|
)
|
|
else:
|
|
return calc_point_on_bezier_spline(
|
|
spline, (distance - total_length), output_tangent, resolution_factor
|
|
)
|
|
total_length += length
|
|
|
|
# NOTE: this is a fallback
|
|
# and should not happen usually
|
|
return bezier_curve_obj.matrix_world @ mathutils.Vector((distance, 0, 0))
|
|
|
|
# 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 is 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 is 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 is 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("found more glyphs objects than expected")
|
|
|
|
# now it must exist
|
|
glyphs_obj = find_objects_by_name(face_obj.children, startswith="glyphs")[0]
|
|
glyphs_obj["face_name"] = obj["face_name"]
|
|
glyphs_obj["font_name"] = obj["font_name"]
|
|
|
|
def get_hash(o):
|
|
return hash(tuple(tuple(v.co) for v in o.data.vertices))
|
|
|
|
for other_obj in find_objects_by_custom_property(
|
|
glyphs_obj.children, "glyph", obj["glyph"]
|
|
):
|
|
if get_hash(other_obj) == get_hash(obj) and not allow_duplicates:
|
|
return other_obj
|
|
|
|
# and now parent it!
|
|
if obj.parent != glyphs_obj:
|
|
obj.parent = glyphs_obj
|
|
|
|
for c in obj.users_collection:
|
|
c.objects.unlink(obj)
|
|
if fontcollection.objects.find(obj.name) < 0:
|
|
fontcollection.objects.link(obj)
|
|
|
|
return obj
|
|
|
|
|
|
def bpy_to_abspath(blender_path):
|
|
return os.path.realpath(bpy.path.abspath(blender_path))
|
|
|
|
|
|
def register_font_from_filepath(filepath):
|
|
from .bimport import get_font_faces_in_file
|
|
|
|
availables = get_font_faces_in_file(filepath)
|
|
|
|
fonts = {}
|
|
for a in availables:
|
|
font_name = a["font_name"]
|
|
face_name = a["face_name"]
|
|
glyph = a["glyph"]
|
|
if font_name not in fonts:
|
|
fonts[font_name] = {}
|
|
if face_name not 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(
|
|
"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 = []
|
|
|
|
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)
|
|
glyph_obj_pointer = bpy.types.PointerProperty(glyph_obj)
|
|
|
|
if glyph_obj == o:
|
|
del o[marker_property]
|
|
|
|
Font.add_glyph(font_name, face_name, glyph_id, glyph_obj_pointer)
|
|
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:
|
|
mff_glyphs = []
|
|
face : Font.FontFace = Font.get_font_face(mff["font_name"], mff["face_name"])
|
|
if face is None:
|
|
print(f"{utils.prefix()}::load_font_from_path({filepath=}, {glyphs=}, {font_name=}, {face_name=}) failed")
|
|
print(f"modified font face {mff=} could not be accessed.")
|
|
continue
|
|
# iterate glyphs
|
|
for g in face.glyphs:
|
|
# iterate alternates
|
|
for glyph in face.glyphs[g]:
|
|
mff_glyphs.append(get_original(glyph))
|
|
if len(mff_glyphs) > 0:
|
|
add_default_metrics_to_objects(mff_glyphs)
|
|
# calculate unit factor
|
|
h = get_glyph_height(mff_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 o.name not 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_and_face in Font.get_loaded_fonts_and_faces():
|
|
found = False
|
|
font_name = font_and_face[0]
|
|
face_name = font_and_face[1]
|
|
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 "text_id" in o.keys():
|
|
# i = o["text_id"]
|
|
# found = False
|
|
# if len(abc3d_data.available_texts) > i:
|
|
# if abc3d_data.available_texts[i].glyphs
|
|
|
|
|
|
def getPreferences(context):
|
|
preferences = context.preferences
|
|
return preferences.addons["abc3d"].preferences
|
|
|
|
|
|
# clear available fonts
|
|
def clear_available_fonts():
|
|
bpy.context.scene.abc3d_data.available_fonts.clear()
|
|
|
|
|
|
def load_installed_fonts():
|
|
preferences = getPreferences(bpy.context)
|
|
font_dir = os.path.join(preferences.assets_dir, "fonts")
|
|
if os.path.exists(font_dir):
|
|
for file in os.listdir(font_dir):
|
|
if file.endswith(".glb") or file.endswith(".gltf"):
|
|
font_path = os.path.join(font_dir, file)
|
|
# ShowMessageBox("Loading Font", "INFO", f"loading font from {font_path}")
|
|
# print(f"loading font from {font_path}")
|
|
# for f in bpy.context.scene.abc3d_data.available_fonts.values():
|
|
# print(f"available font: {f.font_name} {f.face_name}")
|
|
register_font_from_filepath(font_path)
|
|
load_font_from_filepath(font_path)
|
|
|
|
|
|
def register_installed_fonts():
|
|
preferences = getPreferences(bpy.context)
|
|
font_dir = os.path.join(preferences.assets_dir, "fonts")
|
|
if os.path.exists(font_dir):
|
|
for file in os.listdir(font_dir):
|
|
if file.endswith(".glb") or file.endswith(".gltf"):
|
|
font_path = os.path.join(font_dir, file)
|
|
# ShowMessageBox("Loading Font", "INFO", f"loading font from {font_path}")
|
|
# print(f"loading font from {font_path}")
|
|
# for f in bpy.context.scene.abc3d_data.available_fonts.values():
|
|
# print(f"available font: {f.font_name} {f.face_name}")
|
|
register_font_from_filepath(font_path)
|
|
|
|
|
|
message_memory = []
|
|
|
|
|
|
def ShowMessageBox(title="Message Box", icon="INFO", message="", prevent_repeat=False):
|
|
"""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=)
|
|
|
|
"""
|
|
global message_memory
|
|
if prevent_repeat:
|
|
for m in message_memory:
|
|
if m[0] == title and m[1] == icon and m[2] == message:
|
|
return
|
|
message_memory.append([title, icon, message])
|
|
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):
|
|
completely_delete_objects(objs)
|
|
|
|
|
|
def completely_delete_objects(objs, recursive=True):
|
|
for g in objs:
|
|
if type(g) != type(None):
|
|
if recursive:
|
|
try:
|
|
if hasattr(g, "children") and len(g.children) > 0:
|
|
completely_delete_objects(g.children)
|
|
except ReferenceError:
|
|
# not important
|
|
pass
|
|
|
|
try:
|
|
bpy.data.objects.remove(g, do_unlink=True)
|
|
except ReferenceError:
|
|
# not important
|
|
pass
|
|
|
|
|
|
def is_mesh(o):
|
|
return type(o.data) == bpy.types.Mesh
|
|
|
|
|
|
def is_metrics_object(o):
|
|
if f"{utils.prefix()}_type" in o:
|
|
return o[f"{utils.prefix()}_type"] == "metrics"
|
|
return (
|
|
re.match(".*_metrics$", o.name) is not None
|
|
or re.match(".*_metrics.[\d]{3}$", o.name) is not None
|
|
) and is_mesh(o)
|
|
|
|
|
|
def is_text_object(o):
|
|
if f"{utils.prefix()}_type" in o:
|
|
return o[f"{utils.prefix()}_type"] == "textobject"
|
|
for t in bpy.context.scene.abc3d_data.available_texts:
|
|
if o == t.text_object:
|
|
return True
|
|
return False
|
|
|
|
|
|
def is_glyph_object(o):
|
|
if f"{utils.prefix()}_type" in o:
|
|
return o[f"{utils.prefix()}_type"] == "glyph"
|
|
try:
|
|
return (
|
|
type(o.parent) is not type(None)
|
|
and "glyphs" in o.parent.name
|
|
and is_mesh(o)
|
|
and not is_metrics_object(o)
|
|
)
|
|
except ReferenceError:
|
|
return False
|
|
|
|
|
|
def is_glyph(o):
|
|
return is_glyph_object(o)
|
|
|
|
|
|
def update_types():
|
|
scene = bpy.context.scene
|
|
abc3d_data = scene.abc3d_data
|
|
for t in abc3d_data.available_texts:
|
|
t.text_object[f"{utils.prefix()}_type"] = "textobject"
|
|
for g in t.glyphs:
|
|
g.glyph_object[f"{utils.prefix()}_type"] = "glyph"
|
|
|
|
|
|
# blender bound_box vertices
|
|
#
|
|
# 3------7.
|
|
# |`. | `. +y
|
|
# | `2------6 |
|
|
# | | | | |
|
|
# 0---|--4. | +--- +x
|
|
# `. | `.| `.
|
|
# `1------5 `+z
|
|
|
|
|
|
def get_glyph_advance(glyph_obj):
|
|
for c in glyph_obj.children:
|
|
if is_metrics_object(c):
|
|
return abs(c.bound_box[4][0] - c.bound_box[0][0])
|
|
return abs(glyph_obj.bound_box[4][0] - glyph_obj.bound_box[0][0])
|
|
|
|
def get_glyph_prepost_advances(glyph_obj):
|
|
for c in glyph_obj.children:
|
|
if is_metrics_object(c):
|
|
return -1 * c.bound_box[0][0], c.bound_box[4][0]
|
|
return -1 * glyph_obj.bound_box[0][0], glyph_obj.bound_box[4][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, allow_replacement=True):
|
|
availability = Font.test_glyphs_availability(
|
|
font_name, face_name, text
|
|
)
|
|
if isinstance(availability, int):
|
|
if availability == Font.MISSING_FONT:
|
|
print(f"{utils.prefix()}::prepare_text({font_name=}, {face_name=}, {text=}) failed with MISSING_FONT")
|
|
if availability is Font.MISSING_FACE:
|
|
print(f"{utils.prefix()}::prepare_text({font_name=}, {face_name=}, {text=}) failed with MISSING_FACE")
|
|
return False
|
|
loadable = availability.unloaded
|
|
# possibly replace upper and lower case letters with each other
|
|
if len(availability.missing) > 0 and allow_replacement:
|
|
replacement_search = ""
|
|
for m in availability.missing:
|
|
if m.isalpha():
|
|
replacement_search += m.swapcase()
|
|
r = Font.test_availability(font_name, face_name, replacement_search)
|
|
loadable += r.unloaded
|
|
# not update (loaded, missing, files), we only use loadable/maybe later
|
|
|
|
if len(loadable) > 0:
|
|
for filepath in availability.filepaths:
|
|
load_font_from_filepath(filepath, loadable, font_name, face_name)
|
|
return True
|
|
|
|
def predict_actual_text(text_properties):
|
|
availability = Font.test_availability(text_properties.font_name, text_properties.face_name, text_properties.text)
|
|
AVAILABILITY = Font.test_availability(text_properties.font_name, text_properties.face_name, text_properties.text.swapcase())
|
|
t_text = text_properties.text
|
|
for c in availability.missing:
|
|
t_text = t_text.replace(c, "")
|
|
for c in AVAILABILITY.missing:
|
|
t_text = t_text.replace(c, "")
|
|
return t_text
|
|
|
|
def is_bezier(curve):
|
|
if curve.type != "CURVE":
|
|
return False
|
|
if len(curve.data.splines) < 1:
|
|
return False
|
|
for spline in curve.data.splines:
|
|
if spline.type != "BEZIER":
|
|
return False
|
|
return True
|
|
|
|
|
|
text_object_keys = [
|
|
"font_name",
|
|
"face_name",
|
|
"type",
|
|
"text_id",
|
|
"font_size",
|
|
"letter_spacing",
|
|
"distribution_type",
|
|
"orientation",
|
|
"translation",
|
|
"offset",
|
|
"text",
|
|
]
|
|
|
|
glyph_object_keys = [
|
|
"type",
|
|
"glyph_index",
|
|
"glyph_id",
|
|
"text_id",
|
|
"font_name",
|
|
"face_name",
|
|
"font_size",
|
|
"letter_spacing",
|
|
"alternate",
|
|
]
|
|
|
|
ignore_keys_in_text_object_comparison = [
|
|
"type",
|
|
]
|
|
|
|
ignore_keys_in_glyph_object_comparison = [
|
|
"type",
|
|
"glyph_index",
|
|
"font_name",
|
|
"face_name",
|
|
"text_id",
|
|
]
|
|
|
|
ignore_keys_in_glyph_object_transfer = [
|
|
"type",
|
|
"text_id",
|
|
"glyph_index",
|
|
]
|
|
|
|
keys_trigger_regeneration = [
|
|
"font_name",
|
|
"face_name",
|
|
]
|
|
|
|
COMPARE_TEXT_OBJECT_SAME = 0
|
|
COMPARE_TEXT_OBJECT_DIFFER = 1
|
|
COMPARE_TEXT_OBJECT_REGENERATE = 2
|
|
|
|
def find_free_text_id():
|
|
scene = bpy.context.scene
|
|
abc3d_data = scene.abc3d_data
|
|
text_id = 0
|
|
found_free = False
|
|
while not found_free:
|
|
occupied = False
|
|
for t in abc3d_data.available_texts:
|
|
if text_id == t.text_id:
|
|
occupied = True
|
|
if occupied:
|
|
text_id += 1
|
|
else:
|
|
found_free = True
|
|
return text_id
|
|
|
|
def compare_text_properties_to_text_object(text_properties, o):
|
|
for key in text_object_keys:
|
|
if key in ignore_keys_in_text_object_comparison:
|
|
continue
|
|
object_key = get_key(key)
|
|
text_property = text_properties[key] if key in text_properties else getattr(text_properties, key)
|
|
text_object_property = o[object_key] if object_key in o else False
|
|
if text_property != text_object_property:
|
|
if key in keys_trigger_regeneration:
|
|
return COMPARE_TEXT_OBJECT_REGENERATE
|
|
elif key in ["translation", "orientation"]:
|
|
if (
|
|
text_property[0] != text_object_property[0] or
|
|
text_property[1] != text_object_property[1] or
|
|
text_property[2] != text_object_property[2]):
|
|
return COMPARE_TEXT_OBJECT_DIFFER
|
|
# else same
|
|
else:
|
|
return COMPARE_TEXT_OBJECT_DIFFER
|
|
# else same
|
|
return COMPARE_TEXT_OBJECT_SAME
|
|
|
|
|
|
def transfer_text_properties_to_text_object(text_properties, o):
|
|
for key in text_object_keys:
|
|
if key in ignore_keys_in_text_object_comparison:
|
|
continue
|
|
object_key = get_key(key)
|
|
text_property = text_properties[key] if key in text_properties else getattr(text_properties, key)
|
|
o[object_key] = text_property
|
|
o[get_key("type")] = "textobject"
|
|
|
|
|
|
def get_glyph(glyph_id, font_name, face_name, notify_on_replacement=False):
|
|
glyph_tmp = Font.get_glyph(font_name,
|
|
face_name,
|
|
glyph_id,
|
|
-1)
|
|
if glyph_tmp is None:
|
|
space_width = Font.is_space(glyph_id)
|
|
if space_width:
|
|
return space_width
|
|
|
|
message = f"Glyph not found for font_name='{text_properties.font_name}' face_name='{text_properties.face_name}' glyph_id='{glyph_id}'"
|
|
replaced = False
|
|
if glyph_id.isalpha():
|
|
possible_replacement = glyph_id.swapcase()
|
|
glyph_tmp = Font.get_glyph(
|
|
text_properties.font_name,
|
|
text_properties.face_name,
|
|
possible_replacement,
|
|
-1
|
|
)
|
|
if glyph_tmp is not None:
|
|
message = message + f" (replaced with '{possible_replacement}')"
|
|
replaced = True
|
|
|
|
if notify_on_replacement:
|
|
ShowMessageBox(
|
|
title="Glyph replaced" if replaced else "Glyph missing",
|
|
icon="INFO" if replaced else "ERROR",
|
|
message=message,
|
|
prevent_repeat=True,
|
|
)
|
|
if not replaced:
|
|
return None
|
|
|
|
return glyph_tmp.original
|
|
|
|
def get_text_properties(text_id, scene = None):
|
|
if scene is None:
|
|
scene = bpy.context.scene
|
|
abc3d_data = scene.abc3d_data
|
|
for t in abc3d_data.available_texts:
|
|
if text_id == t.text_id:
|
|
return t
|
|
return None
|
|
|
|
def duplicate(obj, data=True, actions=True, add_to_collection=True, collection=None, recursive=True):
|
|
obj_copy = obj.copy()
|
|
if add_to_collection:
|
|
if collection:
|
|
collection.objects.link(obj_copy)
|
|
elif len(obj.users_collection) > 0:
|
|
obj.users_collection[0].objects.link(obj_copy)
|
|
if data and obj.data:
|
|
obj_copy.data = obj.data.copy()
|
|
if actions and obj.animation_data:
|
|
obj_copy.animation_data.action = obj.animation_data.action.copy()
|
|
if recursive and hasattr(obj, "children"):
|
|
for child in obj.children:
|
|
child_copy = duplicate(child)
|
|
child_copy.parent_type = child.parent_type
|
|
child_copy.parent = obj_copy
|
|
# child_copy.matrix_parent_inverse = obj_copy.matrix_world.inverted()
|
|
return obj_copy
|
|
|
|
def transfer_text_object_to_text_properties(text_object, text_properties, id_from_text_properties=True):
|
|
possible_brother_text_id = text_object[get_key("text_id")] if get_key("text_id") in text_object else ""
|
|
for key in text_object_keys:
|
|
if key in ignore_keys_in_text_object_comparison:
|
|
continue
|
|
object_key = get_key(key)
|
|
if id_from_text_properties and key == "text_id":
|
|
text_object[object_key] = text_properties["text_id"]
|
|
else:
|
|
text_object_property = text_object[object_key] if object_key in text_object else False
|
|
if text_object_property is not False:
|
|
text_properties[key] = text_object_property
|
|
|
|
if len(text_object.children) == 0:
|
|
if possible_brother_text_id != text_properties["text_id"] and possible_brother_text_id != "":
|
|
possible_brother_properties = get_text_properties(possible_brother_text_id)
|
|
possible_brother_object = possible_brother_properties.text_object
|
|
if possible_brother_object is not None:
|
|
for child in possible_brother_object.children:
|
|
if is_glyph_object(child):
|
|
child_copy = duplicate(child)
|
|
child_copy.parent_type = child.parent_type
|
|
child_copy.parent = text_object
|
|
parent_to_curve(child_copy, text_object)
|
|
# child_copy.matrix_parent_inverse = text_object.matrix_world.inverted()
|
|
|
|
found_reconstructable_glyphs = False
|
|
glyph_objects_with_indices = []
|
|
required_keys = [
|
|
"glyph_index",
|
|
"glyph_id",
|
|
"type"
|
|
]
|
|
for glyph_object in text_object.children:
|
|
if is_glyph_object(glyph_object):
|
|
has_required_keys = True
|
|
for key in required_keys:
|
|
if get_key(key) not in glyph_object:
|
|
has_required_keys = False
|
|
if has_required_keys:
|
|
inner_node = None
|
|
glyph_id = glyph_object[get_key("glyph_id")]
|
|
for c in glyph_object.children:
|
|
if c.name.startswith(f"{glyph_id}_mesh"):
|
|
inner_node = c
|
|
if inner_node is not None:
|
|
glyph_objects_with_indices.append(glyph_object)
|
|
|
|
glyph_objects_with_indices.sort(key=lambda g: g[get_key("glyph_index")])
|
|
text = ""
|
|
for g in glyph_objects_with_indices:
|
|
text += g[get_key("glyph_id")]
|
|
is_good_text = False
|
|
if len(text) > 0:
|
|
if text == text_properties.text:
|
|
is_good_text = True
|
|
else:
|
|
t_text = predict_actual_text(text_properties)
|
|
if t_text == text:
|
|
is_good_text = True
|
|
if is_good_text:
|
|
text_properties.actual_text = text
|
|
text_properties.glyphs.clear()
|
|
prepare_text(text_properties.font_name, text_properties.face_name, text)
|
|
fail_after_all = False
|
|
for glyph_index, glyph_object in enumerate(glyph_objects_with_indices):
|
|
glyph_id = glyph_object[get_key("glyph_id")]
|
|
# glyph_tmp = Font.get_glyph(text_properties.font_name,
|
|
# text_properties.face_name,
|
|
# glyph_id)
|
|
# glyph = glyph_tmp.original
|
|
glyph_properties = text_properties.glyphs.add()
|
|
|
|
transfer_glyph_object_to_glyph_properties(glyph_object, glyph_properties)
|
|
glyph_properties["glyph_object"] = glyph_object
|
|
glyph_properties["glyph_index"] = glyph_index
|
|
inner_node = None
|
|
for c in glyph_object.children:
|
|
if c.name.startswith(f"{glyph_id}_mesh"):
|
|
inner_node = c
|
|
if inner_node is None:
|
|
fail_after_all = True
|
|
pass
|
|
glyph_properties["glyph_object"] = glyph_object
|
|
if not fail_after_all:
|
|
found_reconstructable_glyphs = True
|
|
|
|
if not found_reconstructable_glyphs:
|
|
text_properties.actual_text = ""
|
|
text_properties.glyphs.clear()
|
|
unfortunate_children = text_object.children
|
|
completely_delete_objects(unfortunate_children)
|
|
def kill_children():
|
|
completely_delete_objects(unfortunate_children)
|
|
run_in_main_thread(kill_children)
|
|
|
|
if "font_name" in text_properties and "face_name" in text_properties:
|
|
font_name = text_properties["font_name"]
|
|
face_name = text_properties["face_name"]
|
|
text_properties.font = f"{font_name} {face_name}"
|
|
|
|
|
|
def link_text_object_with_new_text_properties(text_object, scene=None):
|
|
if scene is None:
|
|
scene = bpy.context.scene
|
|
text_id = find_free_text_id()
|
|
text_properties = scene.abc3d_data.available_texts.add()
|
|
text_properties["text_id"] = text_id
|
|
# text_object[get_key("text_id")] = text_id
|
|
prepare_text(text_object[get_key("font_name")],
|
|
text_object[get_key("face_name")],
|
|
text_object[get_key("text")])
|
|
text_properties.text_object = text_object
|
|
transfer_text_object_to_text_properties(text_object, text_properties)
|
|
|
|
|
|
def test_finding():
|
|
scene = bpy.context.scene
|
|
abc3d_data = scene.abc3d_data
|
|
text_id = find_free_text_id()
|
|
t = abc3d_data.available_texts.add()
|
|
t["text_id"] = text_id
|
|
o = bpy.context.active_object
|
|
transfer_text_object_to_text_properties(o, t)
|
|
|
|
# def detect_texts():
|
|
# scene = bpy.context.scene
|
|
# abc3d_data = scene.abc3d_data
|
|
# for o in bpy.data.objects:
|
|
# if get_key("type") in o \
|
|
# and o[get_key("type") == "textobject" \
|
|
# and o[get_key("t
|
|
|
|
|
|
|
|
def link_text_object_and_text_properties(o, text_properties):
|
|
text_id = text_properties.text_id
|
|
o["text_id"] = text_id
|
|
text_properties.textobject = o
|
|
|
|
def get_glyph_object_property(text_properties, glyph_properties, key):
|
|
if key in glyph_properties:
|
|
return glyph_properties[key]
|
|
if hasattr(glyph_properties, key):
|
|
return getattr(glyph_properties, key)
|
|
return text_properties[key] if key in text_properties else getattr(text_properties, key)
|
|
|
|
def transfer_properties_to_glyph_object(text_properties, glyph_properties, glyph_object):
|
|
for key in glyph_object_keys:
|
|
if key in ignore_keys_in_glyph_object_transfer:
|
|
continue
|
|
object_key = get_key(key)
|
|
glyph_object[object_key] = get_glyph_object_property(text_properties, glyph_properties, key)
|
|
glyph_object[get_key("type")] = "glyph"
|
|
glyph_object[get_key("text_id")] = text_properties["text_id"]
|
|
|
|
def transfer_glyph_object_to_glyph_properties(glyph_object, glyph_properties):
|
|
for key in glyph_object_keys:
|
|
if key in ignore_keys_in_glyph_object_transfer:
|
|
continue
|
|
glyph_properties[key] = glyph_object[get_key(key)]
|
|
glyph_properties["text_id"] = glyph_object[get_key("text_id")]
|
|
|
|
def would_regenerate(text_properties):
|
|
predicted_text = predict_actual_text(text_properties)
|
|
if text_properties.actual_text != predicted_text:
|
|
return True
|
|
if len(text_properties.glyphs) == 0:
|
|
return True
|
|
|
|
for i, g in enumerate(text_properties.glyphs):
|
|
if not hasattr(g.glyph_object, "type"):
|
|
return True
|
|
elif g.glyph_object.type != "EMPTY":
|
|
return True
|
|
# check if perhaps one glyph was deleted
|
|
elif g.glyph_object is None:
|
|
return True
|
|
elif g.glyph_object.parent is None:
|
|
return True
|
|
elif g.glyph_object.parent.users_collection != g.glyph_object.users_collection:
|
|
return True
|
|
elif len(text_properties.text) > i and g.glyph_id != text_properties.text[i]:
|
|
return 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
|
|
):
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def update_matrices(obj):
|
|
if obj.parent is None:
|
|
obj.matrix_world = obj.matrix_basis
|
|
|
|
# else:
|
|
obj.matrix_world = obj.parent.matrix_world * \
|
|
obj.matrix_parent_inverse * \
|
|
obj.matrix_basis
|
|
|
|
def is_or_has_parent(o, parent, if_is_parent=True, max_depth=10):
|
|
if o == parent and if_is_parent:
|
|
return True
|
|
oo = o
|
|
for i in range(0, max_depth):
|
|
oo = oo.parent
|
|
if oo == parent:
|
|
return True
|
|
if oo is None:
|
|
return False
|
|
return False
|
|
|
|
def parent_to_curve(o, c):
|
|
o.parent_type = 'OBJECT'
|
|
o.parent = c
|
|
# o.matrix_parent_inverse = c.matrix_world.inverted()
|
|
|
|
if c.data.use_path and len(c.data.splines) > 0:
|
|
if c.data.splines[0].type == "BEZIER":
|
|
i = -1 if c.data.splines[0].use_cyclic_u else 0
|
|
p = c.data.splines[0].bezier_points[i].co
|
|
o.matrix_parent_inverse.translation = p * -1.0
|
|
elif c.data.splines[0].type == 'NURBS':
|
|
cm = c.to_mesh()
|
|
p = cm.vertices[0].co
|
|
o.matrix_parent_inverse.translation = p * -1.0
|
|
|
|
|
|
def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, can_regenerate=False):
|
|
"""set_text_on_curve
|
|
|
|
An earlier reset cancels the other.
|
|
To disable reset, set both to false.
|
|
|
|
:param text_properties: all information necessary to set text on a curve
|
|
:type text_properties: ABC3D_text_properties
|
|
:param reset_timeout_s: reset external parameters after timeout. (<= 0) = immediate, (> 0) = non-blocking reset timeout in seconds, (False) = no timeout reset
|
|
:type reset_timeout_s: float
|
|
:param reset_depsgraph_n: reset external parameters after n-th depsgraph update. (<= 0) = immediate, (> 0) = reset after n-th depsgraph update, (False) = no depsgraph reset
|
|
:type reset_depsgraph_n: int
|
|
"""
|
|
# NOTE: depsgraph update not locked
|
|
# as we fixed data_path with parent_to_curve trick
|
|
# global lock_depsgraph_update_n_times
|
|
|
|
# starttime = time.perf_counter_ns()
|
|
mom = text_properties.text_object
|
|
if mom.type != "CURVE":
|
|
return False
|
|
|
|
distribution_type = "CALCULATE" if is_bezier(mom) else "FOLLOW_PATH"
|
|
|
|
|
|
# NOTE: following not necessary anymore
|
|
# as we fixed data_path with parent_to_curve trick
|
|
#
|
|
# use_path messes with parenting
|
|
# however, we need it for follow_path
|
|
# https://projects.blender.org/blender/blender/issues/100661
|
|
# previous_use_path = mom.data.use_path
|
|
# if distribution_type == "CALCULATE":
|
|
# mom.data.use_path = False
|
|
# elif distribution_type == "FOLLOW_PATH":
|
|
# mom.data.use_path = True
|
|
|
|
regenerate = can_regenerate and would_regenerate(text_properties)
|
|
|
|
# if we regenerate.... delete objects
|
|
if regenerate and text_properties.get("glyphs"):
|
|
glyph_objects = [g["glyph_object"] for g in text_properties["glyphs"]]
|
|
completely_delete_objects(glyph_objects, True)
|
|
text_properties.glyphs.clear()
|
|
|
|
transfer_text_properties_to_text_object(text_properties, mom)
|
|
|
|
curve_length = get_curve_length(mom)
|
|
advance = text_properties.offset
|
|
glyph_advance = 0
|
|
glyph_index = 0
|
|
is_command = False
|
|
previous_spline_index = -1
|
|
|
|
actual_text = ""
|
|
for i, c in enumerate(text_properties.text):
|
|
face = Font.get_font_face(text_properties.font_name, text_properties.face_name)
|
|
scalor = face.unit_factor * text_properties.font_size
|
|
if c == "\\":
|
|
is_command = True
|
|
continue
|
|
is_newline = False
|
|
if is_command:
|
|
if c == "n":
|
|
is_newline = True
|
|
next_line_advance = get_next_line_advance(mom, advance, glyph_advance)
|
|
if advance == next_line_advance:
|
|
print(
|
|
f"would like to add new line for {text_properties.text} please"
|
|
)
|
|
# TODO: add a new line
|
|
advance = next_line_advance + text_properties.offset
|
|
continue
|
|
is_command = False
|
|
glyph_id = c
|
|
|
|
spline_index = 0
|
|
|
|
############### GET GLYPH
|
|
|
|
glyph_tmp = Font.get_glyph(text_properties.font_name,
|
|
text_properties.face_name,
|
|
glyph_id,
|
|
-1)
|
|
if glyph_tmp is None:
|
|
space_width = Font.is_space(glyph_id)
|
|
if space_width:
|
|
advance = advance + space_width * text_properties.font_size
|
|
continue
|
|
|
|
message = f"Glyph not found for font_name='{text_properties.font_name}' face_name='{text_properties.face_name}' glyph_id='{glyph_id}'"
|
|
replaced = False
|
|
if glyph_id.isalpha():
|
|
possible_replacement = glyph_id.swapcase()
|
|
glyph_tmp = Font.get_glyph(
|
|
text_properties.font_name,
|
|
text_properties.face_name,
|
|
possible_replacement,
|
|
-1
|
|
)
|
|
if glyph_tmp is not None:
|
|
message = message + f" (replaced with '{possible_replacement}')"
|
|
replaced = True
|
|
|
|
if can_regenerate:
|
|
ShowMessageBox(
|
|
title="Glyph replaced" if replaced else "Glyph missing",
|
|
icon="INFO" if replaced else "ERROR",
|
|
message=message,
|
|
prevent_repeat=True,
|
|
)
|
|
if not replaced:
|
|
continue
|
|
|
|
glyph = glyph_tmp.original
|
|
|
|
############### GLYPH PROPERTIES
|
|
|
|
glyph_properties = text_properties.glyphs[glyph_index] if not regenerate else text_properties.glyphs.add()
|
|
|
|
if regenerate:
|
|
glyph_properties["glyph_id"] = glyph_id
|
|
glyph_properties["text_id"] = text_properties.text_id
|
|
glyph_properties["letter_spacing"] = 0
|
|
actual_text += glyph_id
|
|
|
|
############### NODE SCENE MANAGEMENT
|
|
|
|
inner_node = None
|
|
outer_node = None
|
|
if regenerate:
|
|
outer_node = bpy.data.objects.new(f"{glyph_id}", None)
|
|
inner_node = bpy.data.objects.new(f"{glyph_id}_mesh", glyph.data)
|
|
transfer_properties_to_glyph_object(text_properties, glyph_properties, outer_node)
|
|
|
|
# Add into the scene.
|
|
mom.users_collection[0].objects.link(outer_node)
|
|
mom.users_collection[0].objects.link(inner_node)
|
|
|
|
# Parenting is hard.
|
|
inner_node.parent_type = 'OBJECT'
|
|
inner_node.parent = outer_node
|
|
inner_node.matrix_parent_inverse = outer_node.matrix_world.inverted()
|
|
parent_to_curve(outer_node, mom)
|
|
outer_node.hide_set(True)
|
|
|
|
glyph_properties["glyph_object"] = outer_node
|
|
outer_node[f"{utils.prefix()}_glyph_index"] = glyph_index
|
|
else:
|
|
outer_node = glyph_properties.glyph_object
|
|
outer_node[f"{utils.prefix()}_glyph_index"] = glyph_index
|
|
for c in outer_node.children:
|
|
if c.name.startswith(f"{glyph_id}_mesh"):
|
|
inner_node = c
|
|
|
|
############### TRANSFORMS
|
|
|
|
# origins could be shifted
|
|
# so we need to apply a pre_advance
|
|
glyph_pre_advance, glyph_post_advance = get_glyph_prepost_advances(glyph)
|
|
advance += glyph_pre_advance * scalor
|
|
|
|
# check if we want to loop
|
|
applied_advance = advance
|
|
if text_properties.loop_in:
|
|
if applied_advance < 0:
|
|
applied_advance %= curve_length
|
|
|
|
if text_properties.loop_out:
|
|
if applied_advance > curve_length:
|
|
applied_advance %= curve_length
|
|
|
|
if distribution_type == "FOLLOW_PATH":
|
|
outer_node.constraints.new(type="FOLLOW_PATH")
|
|
outer_node.constraints["Follow Path"].target = mom
|
|
outer_node.constraints["Follow Path"].use_fixed_location = True
|
|
outer_node.constraints["Follow Path"].offset_factor = applied_advance / curve_length
|
|
outer_node.constraints["Follow Path"].use_curve_follow = True
|
|
outer_node.constraints["Follow Path"].forward_axis = "FORWARD_X"
|
|
outer_node.constraints["Follow Path"].up_axis = "UP_Y"
|
|
spline_index = 0
|
|
elif distribution_type == "CALCULATE":
|
|
previous_outer_node_rotation_mode = None
|
|
previous_inner_node_rotation_mode = None
|
|
if outer_node.rotation_mode != "QUATERNION":
|
|
outer_node.rotation_mode = "QUATERNION"
|
|
previous_outer_node_rotation_mode = outer_node.rotation_mode
|
|
if inner_node.rotation_mode != "QUATERNION":
|
|
inner_node.rotation_mode = "QUATERNION"
|
|
previous_inner_node_rotation_mode = inner_node.rotation_mode
|
|
|
|
# get info from bezier
|
|
location, tangent, spline_index = calc_point_on_bezier_curve(mom, applied_advance, True, True)
|
|
|
|
# check if we are on a new line
|
|
if spline_index != previous_spline_index:
|
|
is_newline = True
|
|
|
|
# position
|
|
outer_node.location = location + text_properties.translation
|
|
|
|
# orientation / rotation
|
|
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 not text_properties.ignore_orientation else [mathutils.Matrix()]
|
|
|
|
q = mathutils.Quaternion()
|
|
q.rotate(text_properties.orientation)
|
|
outer_node.rotation_quaternion = (motor[0].to_3x3() @ q.to_matrix()).to_quaternion()
|
|
|
|
# # NOTE: supercool but out of scope, as we wouldhave to update it everytime the curve object rotates,
|
|
# # but this would ignore the curve objects orientation:
|
|
# outer_node.rotation_quaternion = (mom.matrix_world.inverted().to_3x3() @ motor[0].to_3x3() @ q.to_matrix()).to_quaternion()
|
|
|
|
# # scale
|
|
outer_node.scale = (scalor, scalor, scalor)
|
|
|
|
if previous_outer_node_rotation_mode:
|
|
outer_node.rotation_mode = previous_outer_node_rotation_mode
|
|
if previous_inner_node_rotation_mode:
|
|
inner_node.rotation_mode = previous_inner_node_rotation_mode
|
|
|
|
# outer_node.hide_viewport = True
|
|
|
|
############### PREPARE FOR THE NEXT
|
|
|
|
glyph_advance = (
|
|
glyph_post_advance * scalor + text_properties.letter_spacing + glyph_properties.letter_spacing
|
|
)
|
|
|
|
# now we need to compensate for curvature
|
|
# otherwise letters will be closer together the curvier the bezier is
|
|
# NOTE: this could be done more efficiently
|
|
curve_compensation = 0
|
|
if distribution_type == "CALCULATE" and (
|
|
not is_newline or spline_index == 0
|
|
):
|
|
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,
|
|
)
|
|
|
|
advance = advance + glyph_advance + curve_compensation
|
|
glyph_index += 1
|
|
previous_spline_index = spline_index
|
|
|
|
if regenerate:
|
|
text_properties["actual_text"] = actual_text
|
|
|
|
return True
|
|
|
|
|
|
verification_object = {
|
|
f"{utils.prefix()}_type": "textobject",
|
|
f"{utils.prefix()}_text_id": 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
|
|
|
|
|
|
# blender bound_box vertices
|
|
#
|
|
# 3------7.
|
|
# |`. | `. +y
|
|
# | `2------6 -z |
|
|
# | | | | `. |
|
|
# 0---|--4. | `+--- +x
|
|
# `. | `.|
|
|
# `1------5
|
|
|
|
|
|
def add_metrics_obj_from_bound_box(glyph, bound_box=None):
|
|
mesh = bpy.data.meshes.new(f"{glyph.name}_metrics") # add the new mesh
|
|
obj = bpy.data.objects.new(mesh.name, mesh)
|
|
obj["font_name"] = glyph["font_name"]
|
|
obj["face_name"] = glyph["face_name"]
|
|
obj["glyph"] = glyph["glyph"]
|
|
obj[f"{utils.prefix()}_type"] = "metrics"
|
|
|
|
# remove already existing metrics
|
|
remove_metrics = []
|
|
for c in glyph.children:
|
|
if is_metrics_object(c):
|
|
remove_metrics.append(c)
|
|
if len(remove_metrics) > 0:
|
|
completely_delete_objects(remove_metrics)
|
|
|
|
col = glyph.users_collection[0]
|
|
col.objects.link(obj)
|
|
# bpy.context.view_layer.objects.active = obj
|
|
obj.parent = glyph
|
|
|
|
if type(bound_box) == type(None):
|
|
bound_box = glyph.bound_box
|
|
|
|
verts = [
|
|
bound_box[0],
|
|
bound_box[1],
|
|
bound_box[2],
|
|
bound_box[3],
|
|
bound_box[4],
|
|
bound_box[5],
|
|
bound_box[6],
|
|
bound_box[7],
|
|
]
|
|
edges = [
|
|
[0, 1],
|
|
[1, 2],
|
|
[2, 3],
|
|
[3, 0],
|
|
[0, 4],
|
|
[1, 5],
|
|
[2, 6],
|
|
[3, 7],
|
|
[4, 5],
|
|
[5, 6],
|
|
[6, 7],
|
|
[7, 4],
|
|
]
|
|
faces = []
|
|
|
|
mesh.from_pydata(verts, edges, faces)
|
|
|
|
|
|
def add_faces_to_metrics(obj):
|
|
mesh = bpy.data.meshes.new(f"{obj.name}") # add the new mesh
|
|
print(f"add_faces_to_metrics for {obj.name}")
|
|
bound_box = bound_box_as_array(obj.bound_box)
|
|
verts = [
|
|
bound_box[0],
|
|
bound_box[1],
|
|
bound_box[2],
|
|
bound_box[3],
|
|
bound_box[4],
|
|
bound_box[5],
|
|
bound_box[6],
|
|
bound_box[7],
|
|
]
|
|
edges = [
|
|
[0, 1],
|
|
[1, 2],
|
|
[2, 3],
|
|
[3, 0],
|
|
[0, 4],
|
|
[1, 5],
|
|
[2, 6],
|
|
[3, 7],
|
|
[4, 5],
|
|
[5, 6],
|
|
[6, 7],
|
|
[7, 4],
|
|
]
|
|
faces = [
|
|
[0, 1, 2],
|
|
[2, 3, 0],
|
|
[2, 6, 7],
|
|
[7, 3, 2],
|
|
[6, 5, 4],
|
|
[4, 7, 6],
|
|
[4, 5, 1],
|
|
[0, 4, 1],
|
|
[1, 5, 6],
|
|
[1, 6, 2],
|
|
[4, 0, 7],
|
|
[7, 0, 3],
|
|
]
|
|
|
|
mesh.from_pydata(verts, edges, faces)
|
|
|
|
old_mesh = obj.data
|
|
obj.data = mesh
|
|
bpy.data.meshes.remove(old_mesh)
|
|
|
|
|
|
def remove_faces_from_metrics(obj):
|
|
mesh = bpy.data.meshes.new(f"{obj.name}") # add the new mesh
|
|
bound_box = bound_box_as_array(obj.bound_box)
|
|
verts = [
|
|
bound_box[0],
|
|
bound_box[1],
|
|
bound_box[2],
|
|
bound_box[3],
|
|
bound_box[4],
|
|
bound_box[5],
|
|
bound_box[6],
|
|
bound_box[7],
|
|
]
|
|
edges = [
|
|
[0, 1],
|
|
[1, 2],
|
|
[2, 3],
|
|
[3, 0],
|
|
[0, 4],
|
|
[1, 5],
|
|
[2, 6],
|
|
[3, 7],
|
|
[4, 5],
|
|
[5, 6],
|
|
[6, 7],
|
|
[7, 4],
|
|
]
|
|
faces = []
|
|
|
|
mesh.from_pydata(verts, edges, faces)
|
|
|
|
old_mesh = obj.data
|
|
obj.data = mesh
|
|
bpy.data.meshes.remove(old_mesh)
|
|
|
|
|
|
# duplicate
|
|
# def remove_metrics_from_selection():
|
|
# for o in bpy.context.selected_objects:
|
|
# is_possibly_glyph = is_mesh(o)
|
|
# if is_possibly_glyph:
|
|
# metrics = []
|
|
# for c in o.children:
|
|
# if is_metrics_object(c):
|
|
# metrics.append(c)
|
|
# completely_delete_objects(metrics)
|
|
|
|
|
|
def get_max_bound_box(bb_1, bb_2=None):
|
|
if type(bb_2) == type(None):
|
|
bb_2 = bb_1
|
|
x_max = max(bb_1[4][0], bb_2[4][0])
|
|
x_min = min(bb_1[0][0], bb_2[0][0])
|
|
y_max = max(bb_1[3][1], bb_2[3][1])
|
|
y_min = min(bb_1[0][1], bb_2[0][1])
|
|
z_max = max(bb_1[1][2], bb_2[1][2])
|
|
z_min = min(bb_1[0][2], bb_2[0][2])
|
|
return [
|
|
mathutils.Vector((x_min, y_min, z_min)),
|
|
mathutils.Vector((x_min, y_min, z_max)),
|
|
mathutils.Vector((x_min, y_max, z_max)),
|
|
mathutils.Vector((x_min, y_max, z_min)),
|
|
mathutils.Vector((x_max, y_min, z_min)),
|
|
mathutils.Vector((x_max, y_min, z_max)),
|
|
mathutils.Vector((x_max, y_max, z_max)),
|
|
mathutils.Vector((x_max, y_max, z_min)),
|
|
]
|
|
|
|
|
|
# blender bound_box vertices
|
|
#
|
|
# 3------7.
|
|
# |`. | `. +y
|
|
# | `2------6 |
|
|
# | | | | |
|
|
# 0---|--4. | +--- +x
|
|
# `. | `.| `.
|
|
# `1------5 `+z
|
|
|
|
|
|
# why not [ [0] * 3 ] * 8
|
|
# https://stackoverflow.com/questions/2397141/how-to-initialize-a-two-dimensional-array-list-of-lists-if-not-using-numpy-in
|
|
def bound_box_as_array(bound_box):
|
|
array = [[0] * 3 for i in range(8)]
|
|
for i in range(0, len(bound_box)):
|
|
for j in range(0, len(bound_box[i])):
|
|
array[i][j] = bound_box[i][j]
|
|
return array
|
|
|
|
|
|
##
|
|
# @brief get_metrics_bound_box
|
|
# generates a metrics bounding box
|
|
# where x-width comes from bb
|
|
# and y-height + z-depth from bb_uebermetrics
|
|
#
|
|
# @param bb
|
|
# @param bb_uebermetrics
|
|
#
|
|
# @return metrics
|
|
def get_metrics_bound_box(bb, bb_uebermetrics):
|
|
metrics = [[0] * 3] * 8
|
|
# hurrays
|
|
if type(bb_uebermetrics) == bpy.types.bpy_prop_array:
|
|
metrics = bound_box_as_array(bb_uebermetrics)
|
|
else:
|
|
metrics = bb_uebermetrics.copy()
|
|
metrics[0][0] = bb[0][0]
|
|
metrics[1][0] = bb[1][0]
|
|
metrics[2][0] = bb[2][0]
|
|
metrics[3][0] = bb[3][0]
|
|
metrics[4][0] = bb[4][0]
|
|
metrics[5][0] = bb[5][0]
|
|
metrics[6][0] = bb[6][0]
|
|
metrics[7][0] = bb[7][0]
|
|
return metrics
|
|
|
|
|
|
def get_metrics_object(o):
|
|
if is_glyph(o):
|
|
for c in o.children:
|
|
if is_metrics_object(c):
|
|
return c
|
|
return None
|
|
|
|
|
|
def get_original(o):
|
|
if hasattr(o, "original"):
|
|
return o.original
|
|
else:
|
|
return o
|
|
|
|
|
|
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 and o is not bpy.context.active_object:
|
|
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 ""
|
|
|
|
def align_origins_to_active_object(objects=None, axis=2):
|
|
if objects is None:
|
|
objects = bpy.context.selected_objects
|
|
if len(objects) == 0:
|
|
return "no objects selected"
|
|
|
|
if bpy.context.active_object is None:
|
|
return "no active object selected"
|
|
|
|
reference_origin_position = bpy.context.active_object.matrix_world.translation[axis]
|
|
|
|
# do it
|
|
for o in objects:
|
|
is_possibly_glyph = is_glyph(o)
|
|
if is_possibly_glyph and o is not bpy.context.active_object:
|
|
if is_mesh(o):
|
|
diff = reference_origin_position - o.matrix_world.translation[axis]
|
|
|
|
for v in o.data.vertices:
|
|
v.co[axis] -= diff
|
|
|
|
o.matrix_world.translation[axis] = reference_origin_position
|
|
|
|
return ""
|
|
|
|
# NOTE:
|
|
# Following code is not necessary anymore,
|
|
# as we derive the advance through metrics
|
|
# boundaries
|
|
|
|
# def divide_vectors(v1=mathutils.Vector((1.0,1.0,1.0)), v2=mathutils.Vector((1.0,1.0,1.0))):
|
|
# return mathutils.Vector([v1[i] / v2[i] for i in range(3)])
|
|
|
|
# def get_origin_shift_metrics(o, axis=0):
|
|
# if not is_metrics_object(o):
|
|
# return False
|
|
# min_value = sys.float_info.max
|
|
# for v in o.data.vertices:
|
|
# if v.co[axis] < min_value:
|
|
# min_value = v.co[axis]
|
|
# if min_value == sys.float_info.max:
|
|
# return False
|
|
# return min_value
|
|
|
|
# def fix_origin_shift_metrics(o, axis=0):
|
|
# shift = get_origin_shift_metrics(o)
|
|
# if not shift:
|
|
# print("False")
|
|
# return False
|
|
# for v in o.data.vertices:
|
|
# v.co[axis] -= shift
|
|
# shift_vector = mathutils.Vector((0.0, 0.0, 0.0))
|
|
# shift_vector[axis] = shift
|
|
# # o.location = o.location - (divide_vectors(v2=o.matrix_world.to_scale()) * (o.matrix_world @ shift_vector))
|
|
# o.matrix_local.translation = o.matrix_local.translation + (shift_vector @ o.matrix_local.inverted())
|
|
# # update_matrices(o)
|
|
# return True
|
|
|
|
|
|
# def fix_objects_metrics_origins(objects=None, axis=0, handle_metrics_directly=True):
|
|
# if objects is None:
|
|
# objects = bpy.context.selected_objects
|
|
# if len(objects) == 0:
|
|
# return "no objects selected"
|
|
|
|
# for o in objects:
|
|
# is_possibly_glyph = is_glyph(o)
|
|
# if is_possibly_glyph:
|
|
# for c in o.children:
|
|
# if is_metrics_object(c):
|
|
# fix_origin_shift_metrics(c, axis)
|
|
# elif is_metrics_object(o) and handle_metrics_directly:
|
|
# fix_origin_shift_metrics(o, axis)
|
|
# return ""
|
|
|
|
# def align_origins_to_metrics(objects=None):
|
|
# if objects is None:
|
|
# objects = bpy.context.selected_objects
|
|
# if len(objects) == 0:
|
|
# return "no objects selected"
|
|
|
|
# for o in objects:
|
|
# is_possibly_glyph = is_glyph(o)
|
|
# if is_possibly_glyph:
|
|
# min_x = 9999999999
|
|
# for c in o.children:
|
|
# if is_metrics_object(c):
|
|
# for v in c.data.vertices:
|
|
# if v.co[0] < min_x:
|
|
# min_x = v.co[0]
|
|
|
|
# metrics_origin_x = c.matrix_world.translation[0] + min_x
|
|
|
|
# diff = metrics_origin_x - o.matrix_world.translation[0]
|
|
|
|
# for v in o.data.vertices:
|
|
# v.co[0] -= diff
|
|
|
|
# o.location += mathutils.Vector((diff, 0.0, 0.0)) @ o.matrix_world.inverted()
|
|
|
|
# for c in o.children:
|
|
# if is_metrics_object(c):
|
|
# c.location -= mathutils.Vector((diff, 0.0, 0.0)) @ o.matrix_world.inverted()
|
|
|
|
# return ""
|
|
|