regenerate on demand, easier orientation, various

This commit is contained in:
themancalledjakob 2024-07-10 16:34:43 +02:00
parent 846b84b6f4
commit 07c38fcdaf
2 changed files with 200 additions and 70 deletions

View file

@ -167,12 +167,30 @@ class FONT3D_glyph_properties(bpy.types.PropertyGroup):
class FONT3D_text_properties(bpy.types.PropertyGroup): class FONT3D_text_properties(bpy.types.PropertyGroup):
def update_callback(self, context): def update_callback(self, context):
butils.set_text_on_curve(self) butils.set_text_on_curve(self)
text_index: bpy.props.IntProperty() # TODO: update when animate
# does not work like this, somehow it does not run in main thread when the text is actually being set
# def get_float(self):
# return self["letter_spacingor"]
# def set_float(self, value):
# print(f"{utils.get_timestamp()} setting float to {value}")
# self["letter_spacingor"] = value
# def fun(text_properties : FONT3D_text_properties):
# # print(text_properties)
# # print(type(text_properties))
# # print(text_properties.letter_spacing)
# print(f"is running ---------------------------------->>>>>>> {text_properties.letter_spacing} and {text_properties.get('letter_spacingor')}")
# # butils.set_text_on_curve(text_properties)
# run_in_main_thread(lambda: fun(self))
text_id: bpy.props.IntProperty()
font_name: bpy.props.StringProperty() font_name: bpy.props.StringProperty()
font_face: bpy.props.StringProperty() font_face: bpy.props.StringProperty()
text_object: bpy.props.PointerProperty(type=bpy.types.Object) text_object: bpy.props.PointerProperty(type=bpy.types.Object)
text: bpy.props.StringProperty() text: bpy.props.StringProperty(
update=update_callback
)
letter_spacing: bpy.props.FloatProperty( letter_spacing: bpy.props.FloatProperty(
# get=get_float,
# set=set_float,
update=update_callback, update=update_callback,
name="Letter Spacing", name="Letter Spacing",
description="Letter Spacing", description="Letter Spacing",
@ -202,7 +220,7 @@ class FONT3D_UL_fonts(bpy.types.UIList):
class FONT3D_UL_texts(bpy.types.UIList): class FONT3D_UL_texts(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
split = layout.split(factor=0.3) split = layout.split(factor=0.3)
split.label(text="Index: %d" % (index)) split.label(text="Id: %d" % (item.text_id))
# custom_icon = "OUTLINER_OB_%s" % item.obj_type # custom_icon = "OUTLINER_OB_%s" % item.obj_type
# split.prop(item, "name", text="", emboss=False, translate=False) # split.prop(item, "name", text="", emboss=False, translate=False)
split.label(text=f"{item.text}") # avoids renaming the item by accident split.label(text=f"{item.text}") # avoids renaming the item by accident
@ -249,15 +267,36 @@ class FONT3D_PT_panel(bpy.types.Panel):
continue continue
remove_me = True remove_me = True
for c in t.text_object.children: for c in t.text_object.children:
if len(c.users_collection) > 0 and (c.get('linked_textobject')) != type(None) and c.get('linked_textobject') == t.text_index: if len(c.users_collection) > 0 and (c.get('linked_textobject')) != type(None) and c.get('linked_textobject') == t.text_id:
remove_me = False remove_me = False
# not sure how to solve this reliably atm,
# we need to reassign the glyph, but also get the proper properties from glyph_properties
# these might be there in t.glyphs, but linked to removed objects
# or they might be lost
if type(next((g for g in t.glyphs if type(g.glyph_object) == type(None)), None)) == type(None):
g = next((g for g in t.glyphs if type(g.glyph_object) == type(None)), None)
for g in t.glyphs:
if type(g) == type(None):
print("IS NONE")
if type(g.glyph_object) == type(None):
print("go IS NONE")
else:
if g.glyph_object == c:
# print(g.glyph_object.name)
pass
if remove_me: if remove_me:
remove_list.append(i) remove_list.append(i)
for i in remove_list: for i in remove_list:
font3d_data.available_texts.remove(i) font3d_data.available_texts.remove(i)
# print(f"{utils.get_timestamp()} ors something") for i, t in enumerate(font3d_data.available_texts):
if context.active_object == t.text_object:
font3d_data.active_text_index = i
if (hasattr(context.active_object, "parent") and
context.active_object.parent == t.text_object):
font3d_data.active_text_index = i
run_in_main_thread(update) run_in_main_thread(update)
@ -425,12 +464,12 @@ class FONT3D_OT_TestFont(bpy.types.Operator):
distribution_type = 'DEFAULT' distribution_type = 'DEFAULT'
t = font3d_data.available_texts.add() text_id = 0
text_index = 0
for i, tt in enumerate(font3d_data.available_texts): for i, tt in enumerate(font3d_data.available_texts):
while text_index == tt.text_index: while text_id == tt.text_id:
text_index = text_index + 1 text_id = text_id + 1
t.text_index = text_index t = font3d_data.available_texts.add()
t.text_id = text_id
t.font_name = font_name t.font_name = font_name
t.font_face = font_face t.font_face = font_face
@ -631,15 +670,21 @@ class FONT3D_OT_CreateFontFromObjects(bpy.types.Operator):
return {'FINISHED'} return {'FINISHED'}
class FONT3D_PT_RightPropertiesPanel(bpy.types.Panel):
class HelloWorldPanel(bpy.types.Panel):
"""Creates a Panel in the Object properties window""" """Creates a Panel in the Object properties window"""
bl_label = "Hello World Panel" bl_label = f"{__name__}"
bl_idname = "OBJECT_PT_hello" bl_idname = "FONT3D_PT_RightPropertiesPanel"
bl_space_type = 'PROPERTIES' bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW' bl_region_type = 'WINDOW'
bl_context = "object" bl_context = "object"
@classmethod
def poll(self,context):
# only show the panel, if it's a textobject or a glyph
is_text = type(next((t for t in context.scene.font3d_data.available_texts if t.text_object == context.active_object), None)) != type(None)
is_glyph = type(next((t for t in context.scene.font3d_data.available_texts if t.text_object == context.active_object.parent), None)) != type(None)
return is_text or is_glyph
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
scene = context.scene scene = context.scene
@ -648,16 +693,27 @@ class HelloWorldPanel(bpy.types.Panel):
obj = context.active_object obj = context.active_object
def is_text():
return type(next((t for t in context.scene.font3d_data.available_texts if t.text_object == context.active_object), None)) != type(None)
def is_glyph():
return type(next((t for t in context.scene.font3d_data.available_texts if t.text_object == context.active_object.parent), None)) != type(None)
textobject = obj if is_text() else obj.parent if is_glyph() else obj
available_text = font3d_data.available_texts[font3d_data.active_text_index]
row = layout.row() row = layout.row()
row.label(text="Hello world!", icon='WORLD_DATA') row.label(text="Hello world!", icon='WORLD_DATA')
row = layout.row() row = layout.row()
row.label(text="Active object is: " + obj.name) row.label(text="Active object is: " + obj.name)
row = layout.row() row = layout.row()
row.prop(obj, "location") row.label(text="text object is: " + textobject.name)
row = layout.row() row = layout.row()
row.operator("mesh.primitive_cube_add") row.label(text=f"active text index is: {font3d_data.active_text_index}")
row = layout.row()
row.prop(available_text, "text")
row = layout.row()
row.prop(available_text, "letter_spacing")
@ -679,35 +735,98 @@ classes = (
FONT3D_OT_ToggleFont3DCollection, FONT3D_OT_ToggleFont3DCollection,
FONT3D_OT_SaveFontToFile, FONT3D_OT_SaveFontToFile,
FONT3D_OT_CreateFontFromObjects, FONT3D_OT_CreateFontFromObjects,
HelloWorldPanel, FONT3D_PT_RightPropertiesPanel,
) )
@persistent
def load_handler(self, dummy):
bpy.app.timers.register(execute_queued_functions)
def load_handler_unload():
bpy.app.timers.unregister(execute_queued_functions)
def register(): def register():
for cls in classes: for cls in classes:
bpy.utils.register_class(cls) bpy.utils.register_class(cls)
bpy.types.Scene.font3d = bpy.props.PointerProperty(type=FONT3D_settings) bpy.types.Scene.font3d = bpy.props.PointerProperty(type=FONT3D_settings)
bpy.types.Scene.font3d_data = bpy.props.PointerProperty(type=FONT3D_data) bpy.types.Scene.font3d_data = bpy.props.PointerProperty(type=FONT3D_data)
bpy.types.Object.__del__ = lambda self: print(f"Bye {self.name}") # bpy.types.Object.__del__ = lambda self: print(f"Bye {self.name}")
print(f"REGISTER {bl_info['name']}") print(f"REGISTER {bl_info['name']}")
bpy.app.timers.register(execute_queued_functions)
# would love to properly auto start this, but IT DOES NOT WORK # auto start
# if load_handler not in bpy.app.handlers.load_post: if load_handler not in bpy.app.handlers.load_post:
# bpy.app.handlers.load_post.append(load_handler) bpy.app.handlers.load_post.append(load_handler)
# clear available fonts # clear available fonts
def clear_available_fonts(): def clear_available_fonts():
bpy.context.scene.font3d_data.available_fonts.clear() bpy.context.scene.font3d_data.available_fonts.clear()
def load_available_fonts():
global shared
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"):
font_path = os.path.join(font_dir, file)
bpy.ops.import_scene.gltf(filepath=font_path)
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 currentObjects:
# must be new
if ("glyph" in o.keys()
and "face_name" in o.keys()
and "font_name" in o.keys()):
glyph_id = o["glyph"]
font_name = o["font_name"]
face_name = o["face_name"]
butils.move_in_fontcollection(
o,
fontcollection)
Font.add_glyph(
font_name,
face_name,
glyph_id,
o)
font3d_data = bpy.context.scene.font3d_data
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"font3d added {font_name}")
else:
remove_list.append(o)
for o in remove_list:
bpy.data.objects.remove(o, do_unlink=True)
run_in_main_thread(clear_available_fonts) run_in_main_thread(clear_available_fonts)
run_in_main_thread(load_available_fonts)
def unregister(): def unregister():
# would love to properly auto start this, but IT DOES NOT WORK
# if load_handler in bpy.app.handlers.load_post:
# bpy.app.handlers.load_post.remove(load_handler)
for cls in classes: for cls in classes:
bpy.utils.unregister_class(cls) bpy.utils.unregister_class(cls)
bpy.app.timers.unregister(execute_queued_functions)
if load_handler in bpy.app.handlers.load_post:
bpy.app.handlers.load_post.remove(load_handler)
load_handler_unload()
del bpy.types.Scene.font3d del bpy.types.Scene.font3d
del bpy.types.Scene.font3d_data del bpy.types.Scene.font3d_data

View file

@ -89,8 +89,8 @@ def align_rotations_auto_pivot(mask, input_rotations, vectors, factors, local_ma
old_rotation = input_rotation.to_matrix() old_rotation = input_rotation.to_matrix()
old_axis = (old_rotation @ local_main_axis).normalized() old_axis = (old_rotation @ local_main_axis).normalized()
new_axis = vector new_axis = vector
rotation_axis = (-(old_axis) + new_axis).normalized() # rotation_axis = (-(old_axis) + new_axis).normalized()
# rotation_axis = old_axis.cross(new_axis).normalized() rotation_axis = old_axis.cross(new_axis).normalized()
if rotation_axis.length < 1e-6: if rotation_axis.length < 1e-6:
# Vectors are linearly dependent, fallback to another axis # Vectors are linearly dependent, fallback to another axis
@ -276,9 +276,6 @@ def find_font_face_object(font_obj, face_name):
return None return None
def move_in_fontcollection(obj, fontcollection): def move_in_fontcollection(obj, fontcollection):
# print(turn_collection_hierarchy_into_path(obj))
# if scene.collection.objects.find(obj.name) >= 0:
# scene.collection.objects.unlink(obj)
for c in obj.users_collection: for c in obj.users_collection:
c.objects.unlink(obj) c.objects.unlink(obj)
if fontcollection.objects.find(obj.name) < 0: if fontcollection.objects.find(obj.name) < 0:
@ -305,6 +302,7 @@ def move_in_fontcollection(obj, fontcollection):
face_obj.empty_display_type = 'PLAIN_AXES' face_obj.empty_display_type = 'PLAIN_AXES'
face_obj["is_face"] = True face_obj["is_face"] = True
fontcollection.objects.link(face_obj) fontcollection.objects.link(face_obj)
# ensure custom properties are set # ensure custom properties are set
face_obj["face_name"] = obj["face_name"] face_obj["face_name"] = obj["face_name"]
face_obj["font_name"] = obj["font_name"] face_obj["font_name"] = obj["font_name"]
@ -367,21 +365,38 @@ def ShowMessageBox(title = "Message Box", icon = 'INFO', message=""):
bpy.context.window_manager.popup_menu(draw, title = title, icon = icon) bpy.context.window_manager.popup_menu(draw, title = title, icon = icon)
def set_text_on_curve(text_properties): def set_text_on_curve(text_properties):
print(f"set text on curve {utils.get_timestamp()} with {text_properties.letter_spacing} and {text_properties.get('letter_spacingor')}")
mom = text_properties.text_object mom = text_properties.text_object
if mom.type != "CURVE": if mom.type != "CURVE":
return False return False
regenerate = False
glyph_objects = [] glyph_objects = []
for g in text_properties.glyphs: for g in text_properties.glyphs:
glyph_objects.append(g.glyph_object) 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) != len(text_properties.glyphs):
regenerate = True
# if we regenerate.... delete objects
if regenerate:
context_override = bpy.context.copy() context_override = bpy.context.copy()
context_override["selected_objects"] = list(glyph_objects) context_override["selected_objects"] = list(glyph_objects)
with bpy.context.temp_override(**context_override): with bpy.context.temp_override(**context_override):
bpy.ops.object.delete() bpy.ops.object.delete()
# remove deleted objects
# this is necessary
for g in glyph_objects: for g in glyph_objects:
if type(g) != type(None):
bpy.data.objects.remove(g, do_unlink=True) bpy.data.objects.remove(g, do_unlink=True)
# bpy.ops.object.delete({"selected_objects": glyph_objects})
text_properties.glyphs.clear() text_properties.glyphs.clear()
#TODO: fix selection with context_override #TODO: fix selection with context_override
@ -394,7 +409,6 @@ def set_text_on_curve(text_properties):
glyph_advance = 0 glyph_advance = 0
is_command = False is_command = False
for i, c in enumerate(text_properties.text): for i, c in enumerate(text_properties.text):
print(f"trying letter ({c})")
if c == '\\': if c == '\\':
is_command = True is_command = True
continue continue
@ -407,16 +421,21 @@ def set_text_on_curve(text_properties):
continue continue
is_command = False is_command = False
glyph_id = c glyph_id = c
glyph = Font.get_glyph(text_properties.font_name, glyph = Font.get_glyph(text_properties.font_name,
text_properties.font_face, text_properties.font_face,
glyph_id) glyph_id)
ob = None
if regenerate:
if glyph == None: if glyph == None:
self.report({'ERROR'}, f"Glyph not found for {font_name} {font_face} {glyph_id}") self.report({'ERROR'}, f"Glyph not found for {font_name} {font_face} {glyph_id}")
continue continue
ob = bpy.data.objects.new(f"{glyph_id}", glyph.data) ob = bpy.data.objects.new(f"{glyph_id}", glyph.data)
ob['linked_textobject'] = text_properties.text_index ob['linked_textobject'] = text_properties.text_id
else:
ob = text_properties.glyphs[i]['glyph_object']
distribution_type = 'CALCULATE' distribution_type = 'CALCULATE'
if distribution_type == 'FOLLOW_PATH': if distribution_type == 'FOLLOW_PATH':
@ -431,7 +450,7 @@ def set_text_on_curve(text_properties):
location, tangent = calc_point_on_bezier_curve(mom, advance, True) location, tangent = calc_point_on_bezier_curve(mom, advance, True)
ob.location = mom.matrix_world @ location ob.location = mom.matrix_world @ location
mask = [0] mask = [0]
input_rotations = [mathutils.Vector((radians(90.0), 0.0, 0.0))] input_rotations = [mathutils.Vector((0.0, 0.0, 0.0))]
vectors = [tangent] vectors = [tangent]
factors = [1.0] factors = [1.0]
local_main_axis = mathutils.Vector((1.0, 0.0, 0.0)) local_main_axis = mathutils.Vector((1.0, 0.0, 0.0))
@ -442,37 +461,29 @@ def set_text_on_curve(text_properties):
local_main_axis) local_main_axis)
if ob.rotation_mode != 'QUATERNION': if ob.rotation_mode != 'QUATERNION':
ob.rotation_mode = 'QUATERNION' ob.rotation_mode = 'QUATERNION'
ob.rotation_quaternion = (mom.matrix_world @ motor[0]).to_quaternion() q = mathutils.Quaternion()
q.rotate(mathutils.Euler((radians(90),0,0)))
ob.rotation_quaternion = (mom.matrix_world @ motor[0] @ q.to_matrix().to_4x4()).to_quaternion()
scalor = 0.001 scalor = 0.001
glyph_advance = (-1 * glyph.bound_box[0][0] + glyph.bound_box[4][0]) * scalor + text_properties.letter_spacing glyph_advance = (-1 * glyph.bound_box[0][0] + glyph.bound_box[4][0]) * scalor + text_properties.letter_spacing
ob.scale = (scalor, scalor, scalor) ob.scale = (scalor, scalor, scalor)
mom.users_collection[0].objects.link(ob)
advance = advance + glyph_advance advance = advance + glyph_advance
if regenerate:
mom.users_collection[0].objects.link(ob)
glyph_data = text_properties.glyphs.add() glyph_data = text_properties.glyphs.add()
glyph_data.glyph_id = glyph_id glyph_data.glyph_id = glyph_id
glyph_data.glyph_object = ob glyph_data.glyph_object = ob
glyph_data.letter_spacing = 0 glyph_data.letter_spacing = 0
ob.select_set(True) ob.select_set(True)
# selected_objects.append(ob)
# selected_objects.append(mom)
if regenerate:
mom.select_set(True) mom.select_set(True)
bpy.context.view_layer.objects.active = mom bpy.context.view_layer.objects.active = mom
bpy.ops.object.parent_set(type='OBJECT') bpy.ops.object.parent_set(type='OBJECT')
# bpy.ops.object.select_all(action='DESELECT')
# for o in previous_selection:
# o.select_set(True)
# context_override = bpy.context.copy()
# context_override["selected_objects"] = selected_objects
# context_override["active_object"] = mom
# with bpy.context.temp_override(**context_override):
# bpy.ops.object.parent_set(type='OBJECT')
return True return True