2298 lines
75 KiB
Python
2298 lines
75 KiB
Python
import importlib
|
|
import os
|
|
import queue
|
|
import re
|
|
|
|
import bpy
|
|
import bpy_types
|
|
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()
|
|
|
|
|
|
# broken
|
|
# 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
|
|
if p1 == h1 and p2 == h2:
|
|
return p1 + t * (p2 - p1)
|
|
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
|
|
if p1 == h1 and p2 == h2:
|
|
return (p2 - p1).normalized()
|
|
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()
|
|
|
|
|
|
# class TestCalcPoint():
|
|
# co: mathutils.Vector
|
|
# handle_left: mathutils.Vector
|
|
# handle_right: mathutils.Vector
|
|
# def __init__(self, co, handle_left=None, handle_right=None):
|
|
# self.co = co
|
|
# if handle_left is not None:
|
|
# self.handle_left = handle_left
|
|
# if handle_right is not None:
|
|
# self.handle_right = handle_right
|
|
|
|
|
|
# a = TestCalcPoint(mathutils.Vector((0,0,0)), handle_right=mathutils.Vector((0,1,0)))
|
|
# b = TestCalcPoint(mathutils.Vector((1,0,0)), handle_left=mathutils.Vector((1,1,0)))
|
|
# c = TestCalcPoint(mathutils.Vector((0,0,0)), handle_right=mathutils.Vector((0,0,0)))
|
|
# d = TestCalcPoint(mathutils.Vector((1,0,0)), handle_left=mathutils.Vector((1,0,0)))
|
|
# calc_point_on_bezier(a,b,0.5)
|
|
|
|
|
|
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 get_real_beziers_and_lengths(bezier_spline_obj, resolution_factor):
|
|
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
|
|
return beziers, lengths, total_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(
|
|
f"{utils.prefix()}::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
|
|
|
|
# in case the handles sit on the points
|
|
# we interpolate the travel from points of the bezier
|
|
# if the bezier points sit on each other we have same issue
|
|
# but that is then to be fixed in the bezier
|
|
if p.handle_left == p.co and len(bezier_spline_obj.bezier_points) > 1:
|
|
beziers, lengths, total_length = get_real_beziers_and_lengths(
|
|
bezier_spline_obj, resolution_factor
|
|
)
|
|
travel_point = calc_point_on_bezier(beziers[0][1], beziers[0][0], 0.001)
|
|
travel = travel_point.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 = get_real_beziers_and_lengths(
|
|
bezier_spline_obj, resolution_factor
|
|
)
|
|
|
|
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)
|
|
|
|
# in case the handles sit on the points
|
|
# we interpolate the travel from points of the bezier
|
|
# if the bezier points sit on each other we have same issue
|
|
# but that is then to be fixed in the bezier
|
|
if p.handle_right == p.co and len(beziers) > 0:
|
|
travel_point = calc_point_on_bezier(beziers[-1][1], beziers[-1][0], 0.001)
|
|
travel = travel_point.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
|
|
]
|
|
|
|
|
|
# not verified
|
|
# 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(
|
|
f"{utils.prefix()}::move_in_fontcollection: 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"{utils.prefix()}:: 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 is_glyph_used(glyph_alternates):
|
|
fontcollection: bpy_types.Collection = bpy.data.collections.get("ABC3D")
|
|
glyph = bpy.types.PointerProperty
|
|
for glyph in glyph_alternates:
|
|
for o in bpy.context.scene.objects:
|
|
# only check other glyphs
|
|
if is_glyph_object(o):
|
|
# then attempt to compare properties
|
|
if (
|
|
get_key("font_name") in o
|
|
and get_key("face_name") in o
|
|
and get_key("glyph_id") in o
|
|
and o[get_key("font_name")] == glyph["font_name"]
|
|
and o[get_key("face_name")] == glyph["face_name"]
|
|
and o[get_key("glyph_id")] == glyph["glyph"]
|
|
):
|
|
# following check is not necessary,
|
|
# but we leave it in for backwards compatibility
|
|
# properties in the fontcollection start with prefix
|
|
# and so they should be caught by previous check
|
|
if fontcollection.users == 0 or not (
|
|
fontcollection in o.users_collection
|
|
and len(o.users_collection) <= 1
|
|
):
|
|
# it's in the scene and has the correct properties
|
|
# it is used
|
|
return True
|
|
# following check is possibly overkill
|
|
# but we also check for objects that use the data
|
|
# and are not glyph objects, in that case we don't pull the data
|
|
# from under their feet
|
|
if is_mesh(o) and o.data == glyph.data:
|
|
# in this case, yes we need to check if it is a glyph in the fontcollection
|
|
if fontcollection.users == 0 or not (
|
|
fontcollection in o.users_collection
|
|
and len(o.users_collection) <= 1
|
|
):
|
|
# bam!
|
|
return True
|
|
# whoosh!
|
|
return False
|
|
|
|
|
|
def clean_text_properties():
|
|
abc3d_data = bpy.context.scene.abc3d_data
|
|
remove_these = []
|
|
for i, text_properties in enumerate(abc3d_data.available_texts):
|
|
if len(text_properties.text_object.users_collection) <= 0:
|
|
remove_these.append(i)
|
|
remove_these.reverse()
|
|
for i in remove_these:
|
|
abc3d_data.available_texts.remove(i)
|
|
|
|
|
|
def clean_fontcollection(fontcollection=None):
|
|
if fontcollection is None:
|
|
fontcollection = bpy.data.collections.get("ABC3D")
|
|
if fontcollection is None:
|
|
print(
|
|
f"{utils.prefix()}::clean_fontcollection: failed beacause fontcollection is none"
|
|
)
|
|
return False
|
|
|
|
collection_fonts = find_objects_by_custom_property(
|
|
fontcollection.all_objects, "is_font", True
|
|
)
|
|
|
|
delete_these_fonts = []
|
|
delete_these_font_faces = []
|
|
delete_these_glyph_moms = []
|
|
for font_and_face in Font.get_loaded_fonts_and_faces():
|
|
font_name = font_and_face[0]
|
|
face_name = font_and_face[1]
|
|
|
|
collection_font_list = find_objects_by_custom_property(
|
|
collection_fonts, "font_name", font_name
|
|
)
|
|
for collection_font in collection_font_list:
|
|
collection_font_face_list = find_objects_by_custom_property(
|
|
collection_font.children, "face_name", face_name
|
|
)
|
|
count_font_faces = 0
|
|
for collection_font_face in collection_font_face_list:
|
|
glyphs_mom_list = find_objects_by_name(
|
|
collection_font_face.children, startswith="glyphs"
|
|
)
|
|
count_glyphs_moms = 0
|
|
for glyphs_mom in glyphs_mom_list:
|
|
if len(glyphs_mom.children) == 0:
|
|
delete_these_glyph_moms.append(glyphs_mom)
|
|
count_glyphs_moms += 1
|
|
if len(collection_font_face.children) == count_glyphs_moms:
|
|
delete_these_font_faces.append(collection_font_face)
|
|
count_font_faces += 1
|
|
if len(collection_font.children) == count_font_faces:
|
|
delete_these_fonts.append(collection_font)
|
|
|
|
completely_delete_objects(delete_these_glyph_moms)
|
|
completely_delete_objects(delete_these_font_faces)
|
|
completely_delete_objects(delete_these_fonts)
|
|
|
|
|
|
def unload_unused_glyph(font_name, face_name, glyph_id, do_clean_fontcollection=True):
|
|
fontcollection: bpy_types.Collection = bpy.data.collections.get("ABC3D")
|
|
glyph_variations = Font.get_glyphs(font_name, face_name, glyph_id)
|
|
if is_glyph_used(glyph_variations):
|
|
return False
|
|
|
|
delete_these = []
|
|
for glyph_pointer in glyph_variations:
|
|
for o in fontcollection.all_objects:
|
|
if (
|
|
is_glyph_object(o)
|
|
and o["font_name"] == font_name
|
|
and o["face_name"] == face_name
|
|
and o["glyph"] == glyph_id
|
|
):
|
|
if len(o.users_collection) <= 1:
|
|
delete_these.append(o)
|
|
completely_delete_objects(delete_these)
|
|
|
|
Font.unloaded_glyph(font_name, face_name, glyph_id)
|
|
if do_clean_fontcollection:
|
|
clean_fontcollection(fontcollection)
|
|
return True
|
|
|
|
|
|
def unload_unused_glyphs(do_clean_fontcollection=True):
|
|
fontcollection: bpy_types.Collection = bpy.data.collections.get("ABC3D")
|
|
if fontcollection is not None:
|
|
for font_and_face in Font.get_loaded_fonts_and_faces():
|
|
font_name = font_and_face[0]
|
|
face_name = font_and_face[1]
|
|
face: Font.FontFace | None = Font.get_font_face(font_name, face_name)
|
|
if face is None:
|
|
print(
|
|
f"{utils.prefix()}::unload_unused_glyphs: face is None {font_name=} {face_name=}"
|
|
)
|
|
continue
|
|
unloaded_these = []
|
|
for glyph_id in face.loaded_glyphs.copy():
|
|
unload_unused_glyph(
|
|
font_name, face_name, glyph_id, do_clean_fontcollection=False
|
|
)
|
|
if do_clean_fontcollection:
|
|
clean_fontcollection(fontcollection)
|
|
|
|
|
|
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"{utils.prefix()}::update_available_fonts: {__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
|
|
except RuntimeError:
|
|
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 (
|
|
o.parent is not 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 is_text_object_legit(text_object):
|
|
must_have_keys = [
|
|
get_key("font_name"),
|
|
get_key("face_name"),
|
|
get_key("text"),
|
|
get_key("type"),
|
|
]
|
|
for key in must_have_keys:
|
|
if key not in text_object:
|
|
return False
|
|
if text_object[get_key("type")] != "textobject":
|
|
return False
|
|
return True
|
|
|
|
|
|
# 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
|
|
if len(mom.users_collection) < 1:
|
|
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:
|
|
n_max = 100
|
|
n = 0
|
|
while (
|
|
(previous_location - new_location).length > glyph_advance
|
|
and psi == si
|
|
and n < n_max
|
|
):
|
|
curve_compensation = curve_compensation - glyph_advance * 0.01
|
|
tmp_new_location, si = calc_point_on_bezier_curve(
|
|
mom,
|
|
advance + glyph_advance + curve_compensation,
|
|
output_tangent=False,
|
|
output_spline_index=True,
|
|
)
|
|
if tmp_new_location == new_location:
|
|
print(
|
|
f"{utils.prefix()}::set_text_on_curve::compensate_curvature while loop overstaying welcome"
|
|
)
|
|
break
|
|
new_location = tmp_new_location
|
|
n += 1
|
|
n = 0
|
|
while (
|
|
(previous_location - new_location).length < glyph_advance
|
|
and psi == si
|
|
and n < n_max
|
|
):
|
|
curve_compensation = curve_compensation + glyph_advance * 0.01
|
|
tmp_new_location, si = calc_point_on_bezier_curve(
|
|
mom,
|
|
advance + glyph_advance + curve_compensation,
|
|
output_tangent=False,
|
|
output_spline_index=True,
|
|
)
|
|
if tmp_new_location == new_location:
|
|
print(
|
|
f"{utils.prefix()}::set_text_on_curve::compensate_curvature while loop overstaying welcome"
|
|
)
|
|
break
|
|
new_location = tmp_new_location
|
|
n += 1
|
|
|
|
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 ""
|