font3d_blender_addon/butils.py
themancalledjakob 5c4ee030d3 metrics io
add functions for metrics saving and loading
we need to add and remove faces. if they don't have faces, blender gltf
loader will ignore the meshes. but we want them in the end only with
vertices and edges.
2024-08-08 11:23:08 +02:00

990 lines
35 KiB
Python

import bpy
import mathutils
import queue
import importlib
import os
import re
# import time # for debugging performance
# then import dependencies for our addon
if "Font" in locals():
importlib.reload(Font)
else:
from .common import Font
if "utils" in locals():
importlib.reload(utils)
else:
from .common import utils
execution_queue = queue.Queue()
# This function can safely be called in another thread.
# The function will be executed when the timer runs the next time.
def run_in_main_thread(function):
execution_queue.put(function)
def execute_queued_functions():
while not execution_queue.empty():
function = execution_queue.get()
function()
return 1.0
def apply_all_transforms(obj):
mb = obj.matrix_basis
if hasattr(obj.data, "transform"):
obj.data.transform(mb)
for c in obj.children:
c.matrix_local = mb @ c.matrix_local
obj.matrix_basis.identity()
def get_parent_collection_names(collection, parent_names):
for parent_collection in bpy.data.collections:
if collection.name in parent_collection.children.keys():
parent_names.append(parent_collection.name)
get_parent_collection_names(parent_collection, parent_names)
return
# Ensure it's a curve object
# TODO: no raising, please
def get_curve_length(curve_obj, resolution = -1):
total_length = 0
curve = curve_obj.data
# Loop through all splines in the curve
for spline in curve.splines:
total_length = total_length + spline.calc_length(resolution=resolution)
return total_length
def get_curve_line_lengths(curve_obj, resolution = -1):
lengths = []
for spline in curve_obj.data.splines:
lengths.append(spline.calc_length(resolution=resolution))
return lengths
def get_next_line_advance(curve_obj, current_advance, previous_glyph_advance, resolution = -1):
curve_line_lengths = get_curve_line_lengths(curve_obj, resolution)
total_length = 0
for cll in curve_line_lengths:
total_length += cll
if current_advance - previous_glyph_advance < total_length:
return total_length
return current_advance
def calc_point_on_bezier(bezier_point_1, bezier_point_2, t):
p1 = bezier_point_1.co
h1 = bezier_point_1.handle_right
p2 = bezier_point_2.co
h2 = bezier_point_2.handle_left
return ((1 - t)**3) * p1 + (3 * t * (1 - t)**2) * h1 + (3 * (t**2) * (1 - t)) * h2 + (t**3) * p2
# same in slightly more lines
# result is equal, performance minimally better perhaps?
# def calc_point_on_bezier(bezier_point_1, bezier_point_2, ratio):
# startPoint = bezier_point_1.co
# controlPoint1 = bezier_point_1.handle_right
# controlPoint2 = bezier_point_2.handle_left
# endPoint = bezier_point_2.co
# remainder = 1 - ratio
# ratioSquared = ratio * ratio
# remainderSquared = remainder * remainder
# startPointMultiplier = remainderSquared * remainder
# controlPoint1Multiplier = remainderSquared * ratio * 3
# controlPoint2Multiplier = ratioSquared * remainder * 3
# endPointMultiplier = ratioSquared * ratio
# return startPoint * startPointMultiplier + controlPoint1 * controlPoint1Multiplier + controlPoint2 * controlPoint2Multiplier + endPoint * endPointMultiplier
def calc_tangent_on_bezier(bezier_point_1, bezier_point_2, t):
p1 = bezier_point_1.co
h1 = bezier_point_1.handle_right
p2 = bezier_point_2.co
h2 = bezier_point_2.handle_left
return (
(-3 * (1 - t)**2) * p1 + (-6 * t * (1 - t) + 3 * (1 - t)**2) * h1 +
(-3 * (t**2) + 6 * t * (1 - t)) * h2 + (3 * t**2) * p2
).normalized()
from math import radians, sqrt, pi, acos
def align_rotations_auto_pivot(mask, input_rotations, vectors, factors, local_main_axis):
output_rotations = [mathutils.Matrix().to_3x3() for _ in range(len(input_rotations))]
for i in mask:
vector = mathutils.Vector(vectors[i]).normalized()
input_rotation = mathutils.Euler(input_rotations[i])
if vector.length < 1e-6:
output_rotations[i] = input_rotation.to_matrix()
continue
old_rotation = input_rotation.to_matrix()
old_axis = (old_rotation @ local_main_axis).normalized()
new_axis = vector
# rotation_axis = (-(old_axis) + new_axis).normalized()
rotation_axis = old_axis.cross(new_axis).normalized()
if rotation_axis.length < 1e-6:
# Vectors are linearly dependent, fallback to another axis
rotation_axis = (old_axis + mathutils.Matrix().to_3x3().col[2]).normalized()
if rotation_axis.length < 1e-6:
# This is now guaranteed to not be zero
rotation_axis = (-(old_axis) + mathutils.Matrix().to_3x3().col[1]).normalized()
# full_angle = radians(sqrt((4 * pow(input_rotation.to_quaternion().dot(mathutils.Quaternion(vectors[i].normalized())), 2) - 3)))
# dot = old_axis.dot(new_axis)
# normalized_diff = (old_axis - new_axis).normalized()
# full_angle = acos(min((old_axis * new_axis + normalized_diff.dot(2)).length, 1))
full_angle = old_axis.angle(new_axis)
angle = factors[i] * full_angle
rotation = mathutils.Quaternion(rotation_axis, angle).to_matrix()
new_rotation_matrix = old_rotation @ rotation
output_rotations[i] = new_rotation_matrix
return [mat.to_4x4() for mat in output_rotations]
def calc_bezier_length(bezier_point_1, bezier_point_2, resolution=20):
step = 1/resolution
previous_p = bezier_point_1.co
length = 0
for i in range(-1, resolution):
t = (i + 1) * step
p = calc_point_on_bezier(bezier_point_1, bezier_point_2, t)
length += (p - previous_p).length
previous_p = p
return length
def calc_point_on_bezier_spline(bezier_spline_obj,
distance,
output_tangent = False,
resolution_factor = 1.0):
# what's the point of just one point
# assert len(bezier_spline_obj.bezier_points) >= 2
# however, maybe let's have it not crash and do this
if len(bezier_spline_obj.bezier_points) < 1:
print("butils::calc_point_on_bezier_spline: whoops, no points. panicking. return 0,0,0")
if output_tangent:
return mathutils.Vector((0,0,0)), mathutils.Vector((1,0,0))
else:
return mathutils.Vector((0,0,0))
if len(bezier_spline_obj.bezier_points) == 1:
p = bezier_spline_obj.bezier_points[0]
travel = (p.handle_left - p.co).normalized() * distance
if output_tangent:
tangent = mathutils.Vector((1,0,0))
return travel, tangent
else:
return travel
if distance <= 0:
p = bezier_spline_obj.bezier_points[0]
travel = (p.co - p.handle_left).normalized() * distance
location = p.co + travel
if output_tangent:
p2 = bezier_spline_obj.bezier_points[1]
tangent = calc_tangent_on_bezier(p, p2, 0)
return location, tangent
else:
return location
beziers = []
lengths = []
total_length = 0
n_bezier_points = len(bezier_spline_obj.bezier_points)
for i in range(0, len(bezier_spline_obj.bezier_points) - 1):
bezier = [ bezier_spline_obj.bezier_points[i],
bezier_spline_obj.bezier_points[i + 1] ]
length = calc_bezier_length(bezier[0],
bezier[1],
int(bezier_spline_obj.resolution_u * resolution_factor))
total_length += length
beziers.append(bezier)
lengths.append(length)
# if total_length > distance:
# break
iterated_distance = 0
for i in range(0, len(beziers)):
if iterated_distance + lengths[i] > distance:
distance_on_bezier = (distance - iterated_distance)
d = distance_on_bezier / lengths[i]
# print(f"i: {i}, d: {d}, distance_on_bezier: {distance_on_bezier}, distance: {distance}")
location = calc_point_on_bezier(beziers[i][0],
beziers[i][1],
d)
if output_tangent:
tangent = calc_tangent_on_bezier(beziers[i][0],
beziers[i][1],
d)
return location, tangent
else:
return location
iterated_distance += lengths[i]
# if we are here, the point is outside the spline
last_i = len(beziers) - 1
p = beziers[last_i][1]
travel = (p.handle_right - p.co).normalized() * (distance - total_length)
location = p.co + travel
if output_tangent:
tangent = calc_tangent_on_bezier(beziers[last_i][0],
p,
1)
return location, tangent
else:
return location
def calc_point_on_bezier_curve(bezier_curve_obj,
distance,
output_tangent = False,
resolution_factor = 1.0):
curve = bezier_curve_obj.data
# Loop through all splines in the curve
total_length = 0
for i, spline in enumerate(curve.splines):
resolution = int(spline.resolution_u * resolution_factor)
length = spline.calc_length(resolution=resolution)
if total_length + length > distance or i == len(curve.splines) - 1:
return calc_point_on_bezier_spline(spline,
(distance - total_length),
output_tangent,
resolution_factor)
total_length += length
# TODO: can this fail?
# def get_objects_by_name(name, startswith="", endswith=""):
# return [obj for obj in bpy.context.scene.objects if obj.name.startswith(startswith) and if obj.name.endswith(endswith)]
def find_objects_by_name(
objects,
equals="",
contains="",
startswith="",
endswith=""):
# handle equals
if equals != "":
index = objects.find(equals)
if index >= 0:
return [objects[index]]
return []
# handle others is more permissive
return [obj for obj in objects if obj.name.startswith(startswith) and obj.name.endswith(endswith) and obj.name.find(contains) >= 0]
def find_objects_by_custom_property(
objects,
property_name="",
property_value=""):
return [obj for obj in objects if property_name in obj and obj[property_name] == property_value]
def turn_collection_hierarchy_into_path(obj):
parent_collection = obj.users_collection[0]
parent_names = []
parent_names.append(parent_collection.name)
get_parent_collection_names(parent_collection, parent_names)
parent_names.reverse()
return '\\'.join(parent_names)
def find_font_object(fontcollection, font_name):
fonts = find_objects_by_custom_property(fontcollection.objects,
"is_font",
True)
for font in fonts:
if font["font_name"] == font_name and font.parent == None:
return font
return None
def find_font_face_object(font_obj, face_name):
faces = find_objects_by_custom_property(font_obj.children,
"is_face",
True)
for face in faces:
if face["face_name"] == face_name:
return face
return None
def move_in_fontcollection(obj, fontcollection, allow_duplicates=False):
# parent nesting structure
# the font object
font_obj = find_font_object(fontcollection,
obj["font_name"])
if font_obj == None:
font_obj = bpy.data.objects.new(obj["font_name"], None)
font_obj.empty_display_type = 'PLAIN_AXES'
fontcollection.objects.link(font_obj)
# ensure custom properties are set
font_obj["font_name"] = obj["font_name"]
font_obj["is_font"] = True
# the face object as a child of font object
face_obj = find_font_face_object(font_obj,
obj["face_name"])
if face_obj == None:
face_obj = bpy.data.objects.new(obj["face_name"], None)
face_obj.empty_display_type = 'PLAIN_AXES'
face_obj["is_face"] = True
fontcollection.objects.link(face_obj)
# ensure custom properties are set
face_obj["face_name"] = obj["face_name"]
face_obj["font_name"] = obj["font_name"]
if face_obj.parent != font_obj:
face_obj.parent = font_obj
# create glyphs if it does not exist
glyphs_objs = find_objects_by_name(face_obj.children, startswith="glyphs")
if len(glyphs_objs) <= 0:
glyphs_obj = bpy.data.objects.new("glyphs", None)
glyphs_obj.empty_display_type = 'PLAIN_AXES'
fontcollection.objects.link(glyphs_obj)
glyphs_obj.parent = face_obj
elif len(glyphs_objs) > 1:
print(f"found more glyphs objects than expected")
# now it must exist
glyphs_obj = find_objects_by_name(face_obj.children, startswith="glyphs")[0]
glyphs_obj["face_name"] = obj["face_name"]
glyphs_obj["font_name"] = obj["font_name"]
def get_hash(o):
return hash(tuple(tuple(v.co) for v in o.data.vertices ))
for other_obj in find_objects_by_custom_property(glyphs_obj.children, "glyph", obj["glyph"]):
if get_hash(other_obj) == get_hash(obj) and not allow_duplicates:
return other_obj
# and now parent it!
if obj.parent != glyphs_obj:
obj.parent = glyphs_obj
for c in obj.users_collection:
c.objects.unlink(obj)
if fontcollection.objects.find(obj.name) < 0:
fontcollection.objects.link(obj)
return obj
def load_font_from_filepath(filepath):
if not filepath.endswith(".glb") and not filepath.endswith(".gltf"):
ShowMessageBox(f"{bl_info['name']} Font loading error", 'ERROR', f"Filepath({filepath}) is not a *.glb or *.gltf file")
return False
font3d_data = bpy.context.scene.font3d_data
allObjectsBefore = []
for ob in bpy.data.objects:
allObjectsBefore.append(ob.name)
bpy.ops.import_scene.gltf(filepath=filepath)
fontcollection = bpy.data.collections.get("Font3D")
if fontcollection is None:
fontcollection = bpy.data.collections.new("Font3D")
remove_list = []
all_objects = []
for o in bpy.data.objects:
all_objects.append(o)
for o in all_objects:
if o.name not in allObjectsBefore:
# must be new
if ("glyph" in o.keys()
and "face_name" in o.keys()
and "font_name" in o.keys()
and not ("type" in o.keys() and o["type"] == "metrics")
and not is_metrics_object(o)
):
glyph_id = o["glyph"]
font_name = o["font_name"]
face_name = o["face_name"]
glyph_obj = move_in_fontcollection(
o,
fontcollection)
Font.add_glyph(
font_name,
face_name,
glyph_id,
glyph_obj)
for c in o.children:
if is_metrics_object(c):
add_metrics_obj_from_bound_box(glyph_obj,
bound_box_as_array(c.bound_box))
if glyph_obj != o:
remove_list.append(o)
found = False
for f in font3d_data.available_fonts.values():
if f.font_name == font_name:
found = True
break
if not found:
f = font3d_data.available_fonts.add()
f.font_name = font_name
print(f"{__name__} added {font_name}")
else:
remove_list.append(o)
for o in remove_list:
bpy.data.objects.remove(o, do_unlink=True)
print(f"{__name__}: loaded fonts")
def getPreferences(context):
preferences = context.preferences
return preferences.addons['font3d'].preferences
# clear available fonts
def clear_available_fonts():
bpy.context.scene.font3d_data.available_fonts.clear()
def load_available_fonts():
preferences = getPreferences(bpy.context)
currentObjects = []
for ob in bpy.data.objects:
currentObjects.append(ob.name)
print(f"assets folder: {preferences.assets_dir}")
font_dir = f"{preferences.assets_dir}/fonts"
for file in os.listdir(font_dir):
if file.endswith(".glb") or file.endswith(".gltf"):
font_path = os.path.join(font_dir, file)
load_font_from_filepath(font_path)
def ShowMessageBox(title = "Message Box", icon = 'INFO', message=""):
"""Show a simple message box
taken from `Link here <https://blender.stackexchange.com/questions/169844/multi-line-text-box-with-popup-menu>`_
:param title: The title shown in the message top bar
:type title: str
:param icon: The icon to be shown in the message top bar
:type icon: str
:param message: lines of text to display, a.k.a. the message
:type message: str or (str, str, ..)
TIP: Check `Link blender icons <https://docs.blender.org/api/current/bpy_types_enum_items/icon_items.html>`_ for icons you can use
TIP: Or even better, check `Link this addons <https://docs.blender.org/manual/en/latest/addons/development/icon_viewer.html>`_ to also see the icons.
usage:
.. code-block:: python
myLines=("line 1","line 2","line 3")
butils.ShowMessageBox(message=myLines)
or:
.. code-block:: python
butils.ShowMessageBox(title="",message=("AAAAAH","NOOOOO"),icon=)
"""
myLines=message
def draw(self, context):
if isinstance(myLines, str):
self.layout.label(text=myLines)
elif hasattr(myLines, "__iter__"):
for n in myLines:
self.layout.label(text=n)
bpy.context.window_manager.popup_menu(draw, title = title, icon = icon)
def completely_delete_objects(objs):
context_override = bpy.context.copy()
context_override["selected_objects"] = list(objs)
with bpy.context.temp_override(**context_override):
bpy.ops.object.delete()
# remove deleted objects
# this is necessary
for g in objs:
if type(g) != type(None):
try:
bpy.data.objects.remove(g, do_unlink=True)
except ReferenceError as e:
# not important
pass
def is_mesh(o):
return type(o.data) == bpy.types.Mesh
def is_metrics_object(o):
return (re.match("[\w]*_metrics$", o.name) != None or re.match("[\w]*_metrics.[\d]{3}$", o.name) != None) and is_mesh(o)
def is_glyph(o):
try:
return type(o.parent) is not type(None) \
and "glyphs" in o.parent.name \
and is_mesh(o) \
and not is_metrics_object(o)
except ReferenceError as e:
return False
# blender bound_box vertices
#
# 3------7.
# |`. | `. +y
# | `2------6 |
# | | | | |
# 0---|--4. | +--- +x
# `. | `.| `.
# `1------5 `+z
def get_glyph_advance(glyph_obj):
for c in glyph_obj.children:
if is_metrics_object(c):
return abs(c.bound_box[4][0] - c.bound_box[0][0])
return abs(glyph_obj.bound_box[4][0] - glyph_obj.bound_box[0][0])
def set_text_on_curve(text_properties):
# starttime = time.perf_counter_ns()
mom = text_properties.text_object
if mom.type != "CURVE":
return False
regenerate = False
glyph_objects = []
for i, g in enumerate(text_properties.glyphs):
glyph_objects.append(g.glyph_object)
# check if perhaps one glyph was deleted
if (type(g.glyph_object) == type(None)
or type(g.glyph_object.parent) == type(None)
or g.glyph_object.parent.users_collection != g.glyph_object.users_collection):
regenerate = True
if len(text_properties.text) > i and g.glyph_id != text_properties.text[i]:
regenerate = True
if len(text_properties.text) != len(text_properties.glyphs):
regenerate = True
# if we regenerate.... delete objects
if regenerate:
completely_delete_objects(glyph_objects)
# context_override = bpy.context.copy()
# context_override["selected_objects"] = list(glyph_objects)
# with bpy.context.temp_override(**context_override):
# bpy.ops.object.delete()
# # remove deleted objects
# # this is necessary
# for g in glyph_objects:
# if type(g) != type(None):
# bpy.data.objects.remove(g, do_unlink=True)
text_properties.glyphs.clear()
#TODO: fix selection with context_override
previous_selection = bpy.context.selected_objects
bpy.ops.object.select_all(action='DESELECT')
selected_objects = []
curve_length = get_curve_length(mom)
advance = 0
glyph_advance = 0
is_command = False
for i, c in enumerate(text_properties.text):
if c == '\\':
is_command = True
continue
if is_command:
if c == 'n':
next_line_advance = get_next_line_advance(mom, advance, glyph_advance)
if advance == next_line_advance:
# self.report({'INFO'}, f"would like to add new line for {text_properties.text} please")
print(f"would like to add new line for {text_properties.text} please")
# TODO: add a new line
advance = next_line_advance
continue
is_command = False
glyph_id = c
glyph = Font.get_glyph(text_properties.font_name,
text_properties.font_face,
glyph_id)
if glyph == None:
# self.report({'ERROR'}, f"Glyph not found for {font_name} {font_face} {glyph_id}")
print(f"Glyph not found for {text_properties.font_name} {text_properties.font_face} {glyph_id}")
continue
ob = None
if regenerate:
ob = bpy.data.objects.new(f"{glyph_id}", glyph.data)
ob['linked_textobject'] = text_properties.text_id
else:
ob = text_properties.glyphs[i]['glyph_object']
distribution_type = 'CALCULATE'
if distribution_type == 'FOLLOW_PATH':
ob.constraints.new(type='FOLLOW_PATH')
ob.constraints["Follow Path"].target = mom
ob.constraints["Follow Path"].use_fixed_location = True
ob.constraints["Follow Path"].offset_factor = advance / curve_length
ob.constraints["Follow Path"].use_curve_follow = True
ob.constraints["Follow Path"].forward_axis = "FORWARD_X"
ob.constraints["Follow Path"].up_axis = "UP_Y"
elif distribution_type == 'CALCULATE':
location, tangent = calc_point_on_bezier_curve(mom, advance, True)
ob.location = mom.matrix_world @ (location + text_properties.translation)
if not text_properties.ignore_orientation:
mask = [0]
input_rotations = [mathutils.Vector((0.0, 0.0, 0.0))]
vectors = [tangent]
factors = [1.0]
local_main_axis = mathutils.Vector((1.0, 0.0, 0.0))
motor = align_rotations_auto_pivot(mask,
input_rotations,
vectors,
factors,
local_main_axis)
if ob.rotation_mode != 'QUATERNION':
ob.rotation_mode = 'QUATERNION'
q = mathutils.Quaternion()
q.rotate(text_properties.orientation)
ob.rotation_quaternion = (mom.matrix_world @ motor[0] @ q.to_matrix().to_4x4()).to_quaternion()
else:
q = mathutils.Quaternion()
q.rotate(text_properties.orientation)
ob.rotation_quaternion = q
# ob.rotation_quaternion = (mom.matrix_world @ q.to_matrix().to_4x4()).to_quaternion()
scalor = 0.001 * text_properties.font_size
glyph_advance = get_glyph_advance(glyph) * scalor + text_properties.letter_spacing
# now we need to compensate for curvature
# otherwise letters will be closer together the curvier the bezier is
# this could be done more efficiently, but whatever
curve_compensation = 0
if text_properties.compensate_curvature:
previous_location = calc_point_on_bezier_curve(mom, advance, False)
new_location = calc_point_on_bezier_curve(mom, advance + glyph_advance, False)
while (previous_location - new_location).length > glyph_advance:
curve_compensation = curve_compensation - glyph_advance * 0.01
new_location = calc_point_on_bezier_curve(mom, advance + glyph_advance + curve_compensation, False)
while (previous_location - new_location).length < glyph_advance:
curve_compensation = curve_compensation + glyph_advance * 0.01
new_location = calc_point_on_bezier_curve(mom, advance + glyph_advance + curve_compensation, False)
ob.scale = (scalor, scalor, scalor)
advance = advance + glyph_advance + curve_compensation
if regenerate:
mom.users_collection[0].objects.link(ob)
glyph_data = text_properties.glyphs.add()
glyph_data.glyph_id = glyph_id
glyph_data.glyph_object = ob
glyph_data.letter_spacing = 0
ob.select_set(True)
if regenerate:
mom.select_set(True)
bpy.context.view_layer.objects.active = mom
bpy.ops.object.parent_set(type='OBJECT')
# endtime = time.perf_counter_ns()
# elapsedtime = endtime - starttime
return True
# blender bound_box vertices
#
# 3------7.
# |`. | `. +y
# | `2------6 -z |
# | | | | `. |
# 0---|--4. | `+--- +x
# `. | `.|
# `1------5
def add_metrics_obj_from_bound_box(glyph, bound_box=None):
mesh = bpy.data.meshes.new(f"{glyph.name}_metrics") # add the new mesh
obj = bpy.data.objects.new(mesh.name, mesh)
obj["font_name"] = glyph["font_name"]
obj["face_name"] = glyph["face_name"]
obj["glyph"] = glyph["glyph"]
obj["type"] = "metrics"
col = glyph.users_collection[0]
col.objects.link(obj)
# bpy.context.view_layer.objects.active = obj
obj.parent = glyph
if type(bound_box) == type(None):
bound_box = glyph.bound_box
verts = [bound_box[0],
bound_box[1],
bound_box[2],
bound_box[3],
bound_box[4],
bound_box[5],
bound_box[6],
bound_box[7],
]
edges = [[0,1],[1,2],[2,3],[3,0],
[0,4],[1,5],[2,6],[3,7],
[4,5],[5,6],[6,7],[7,4],
]
faces = []
mesh.from_pydata(verts, edges, faces)
def add_faces_to_metrics(obj):
mesh = bpy.data.meshes.new(f"{obj.name}") # add the new mesh
bound_box = bound_box_as_array(obj.bound_box)
verts = [bound_box[0],
bound_box[1],
bound_box[2],
bound_box[3],
bound_box[4],
bound_box[5],
bound_box[6],
bound_box[7],
]
edges = [[0,1],[1,2],[2,3],[3,0],
[0,4],[1,5],[2,6],[3,7],
[4,5],[5,6],[6,7],[7,4],
]
faces = [
[0,1,2], [2,3,0],
[2,6,7], [7,3,2],
[6,5,4], [4,7,6],
[4,5,1], [0,4,1],
[1,5,6], [1,6,2],
[4,0,7], [7,0,3],
]
mesh.from_pydata(verts, edges, faces)
old_mesh = obj.data
obj.data = mesh
bpy.data.meshes.remove(old_mesh)
def remove_faces_from_metrics(obj):
mesh = bpy.data.meshes.new(f"{obj.name}") # add the new mesh
bound_box = bound_box_as_array(obj.bound_box)
verts = [bound_box[0],
bound_box[1],
bound_box[2],
bound_box[3],
bound_box[4],
bound_box[5],
bound_box[6],
bound_box[7],
]
edges = [[0,1],[1,2],[2,3],[3,0],
[0,4],[1,5],[2,6],[3,7],
[4,5],[5,6],[6,7],[7,4],
]
faces = [
]
mesh.from_pydata(verts, edges, faces)
old_mesh = obj.data
obj.data = mesh
bpy.data.meshes.remove(old_mesh)
# duplicate
# def remove_metrics_from_selection():
# for o in bpy.context.selected_objects:
# is_possibly_glyph = is_mesh(o)
# if is_possibly_glyph:
# metrics = []
# for c in o.children:
# if is_metrics_object(c):
# metrics.append(c)
# completely_delete_objects(metrics)
def get_max_bound_box(bb_1, bb_2 = None):
if type(bb_2) == type(None):
bb_2 = bb_1
x_max = max(bb_1[4][0], bb_2[4][0])
x_min = min(bb_1[0][0], bb_2[0][0])
y_max = max(bb_1[3][1], bb_2[3][1])
y_min = min(bb_1[0][1], bb_2[0][1])
z_max = max(bb_1[1][2], bb_2[1][2])
z_min = min(bb_1[0][2], bb_2[0][2])
return [
mathutils.Vector((x_min, y_min, z_min)),
mathutils.Vector((x_min, y_min, z_max)),
mathutils.Vector((x_min, y_max, z_max)),
mathutils.Vector((x_min, y_max, z_min)),
mathutils.Vector((x_max, y_min, z_min)),
mathutils.Vector((x_max, y_min, z_max)),
mathutils.Vector((x_max, y_max, z_max)),
mathutils.Vector((x_max, y_max, z_min)),
]
# blender bound_box vertices
#
# 3------7.
# |`. | `. +y
# | `2------6 |
# | | | | |
# 0---|--4. | +--- +x
# `. | `.| `.
# `1------5 `+z
# why not [ [0] * 3 ] * 8
# https://stackoverflow.com/questions/2397141/how-to-initialize-a-two-dimensional-array-list-of-lists-if-not-using-numpy-in
def bound_box_as_array(bound_box):
array = [ [0] * 3 for i in range(8) ]
for i in range(0, len(bound_box)):
for j in range(0, len(bound_box[i])):
array[i][j] = bound_box[i][j]
return array
##
# @brief get_metrics_bound_box
# generates a metrics bounding box
# where x-width comes from bb
# and y-height + z-depth from bb_uebermetrics
#
# @param bb
# @param bb_uebermetrics
#
# @return metrics
def get_metrics_bound_box(bb, bb_uebermetrics):
metrics = [[0] * 3] * 8
# hurrays
if type(bb_uebermetrics) == bpy.types.bpy_prop_array:
metrics = bound_box_as_array(bb_uebermetrics)
else:
metrics = bb_uebermetrics.copy()
metrics[0][0] = bb[0][0]
metrics[1][0] = bb[1][0]
metrics[2][0] = bb[2][0]
metrics[3][0] = bb[3][0]
metrics[4][0] = bb[4][0]
metrics[5][0] = bb[5][0]
metrics[6][0] = bb[6][0]
metrics[7][0] = bb[7][0]
return metrics
def add_default_metrics_to_objects(objects=None, overwrite_existing=False):
if type(objects) == type(None):
objects=bpy.context.selected_objects
targets = []
reference_bound_box = None
for o in objects:
is_possibly_glyph = is_glyph(o)
if is_possibly_glyph:
metrics = []
for c in o.children:
if is_metrics_object(c):
metrics.append(c)
if len(metrics) == 0:
targets.append(o)
reference_bound_box = get_max_bound_box(o.bound_box, reference_bound_box)
elif len(metrics) >= 0 and overwrite_existing:
completely_delete_objects(metrics)
targets.append(o)
reference_bound_box = get_max_bound_box(o.bound_box, reference_bound_box)
else:
for m in metrics:
reference_bound_box = get_max_bound_box(m.bound_box, reference_bound_box)
for t in targets:
bound_box = get_metrics_bound_box(t.bound_box, reference_bound_box)
add_metrics_obj_from_bound_box(t, bound_box)
def remove_metrics_from_objects(objects=None):
if type(objects) == type(None):
objects=bpy.context.selected_objects
metrics = []
for o in objects:
for c in o.children:
if is_metrics_object(c):
metrics.append(c)
completely_delete_objects(metrics)
def align_metrics_of_objects_to_active_object(objects=None):
if type(objects) == type(None):
objects=bpy.context.selected_objects
if len(objects) == 0:
return "no objects selected"
# define the reference_bound_box
reference_bound_box = None
if type(bpy.context.active_object) == type(None):
return "no active_object, but align_to_active_object is True"
for c in bpy.context.active_object.children:
if is_metrics_object(c):
reference_bound_box = bound_box_as_array(c.bound_box)
break
if type(reference_bound_box) == type(None):
if not is_mesh(bpy.context.active_object):
return "active_object is not a mesh and does not have a metrics child"
reference_bound_box = bound_box_as_array(bpy.context.active_object.bound_box)
# do it
for o in objects:
is_possibly_glyph = is_glyph(o)
if is_possibly_glyph:
metrics = []
for c in o.children:
if is_metrics_object(c):
metrics.append(c)
bb = None
if len(metrics) == 0:
bb = get_metrics_bound_box(o.bound_box,
reference_bound_box)
else:
bb = get_metrics_bound_box(metrics[0].bound_box,
reference_bound_box)
if len(metrics) > 0:
completely_delete_objects(metrics)
add_metrics_obj_from_bound_box(o, bb)
return ""
def align_metrics_of_objects(objects=None):
if type(objects) == type(None):
objects=bpy.context.selected_objects
if len(objects) == 0:
return "no objects selected"
targets = []
reference_bound_box = None
for o in objects:
is_possibly_glyph = is_glyph(o)
if is_possibly_glyph:
metrics = []
for c in o.children:
if is_metrics_object(c):
metrics.append(c)
if len(metrics) == 0:
reference_bound_box = get_max_bound_box(o.bound_box,
reference_bound_box)
elif len(metrics) > 0:
reference_bound_box = get_max_bound_box(metrics[0].bound_box,
reference_bound_box)
targets.append(o)
for t in targets:
metrics = []
for c in t.children:
if is_metrics_object(c):
metrics.append(c)
bound_box = None
if len(metrics) == 0:
bound_box = get_metrics_bound_box(t.bound_box,
reference_bound_box)
else:
bound_box = get_metrics_bound_box(metrics[0].bound_box,
reference_bound_box)
completely_delete_objects(metrics)
add_metrics_obj_from_bound_box(t, bound_box)
return ""