regenerate on demand, easier orientation, various
This commit is contained in:
parent
846b84b6f4
commit
07c38fcdaf
2 changed files with 200 additions and 70 deletions
173
__init__.py
173
__init__.py
|
@ -167,12 +167,30 @@ class FONT3D_glyph_properties(bpy.types.PropertyGroup):
|
|||
class FONT3D_text_properties(bpy.types.PropertyGroup):
|
||||
def update_callback(self, context):
|
||||
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_face: bpy.props.StringProperty()
|
||||
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(
|
||||
# get=get_float,
|
||||
# set=set_float,
|
||||
update=update_callback,
|
||||
name="Letter Spacing",
|
||||
description="Letter Spacing",
|
||||
|
@ -202,7 +220,7 @@ class FONT3D_UL_fonts(bpy.types.UIList):
|
|||
class FONT3D_UL_texts(bpy.types.UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
|
||||
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
|
||||
# split.prop(item, "name", text="", emboss=False, translate=False)
|
||||
split.label(text=f"{item.text}") # avoids renaming the item by accident
|
||||
|
@ -249,15 +267,36 @@ class FONT3D_PT_panel(bpy.types.Panel):
|
|||
continue
|
||||
remove_me = True
|
||||
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
|
||||
# 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:
|
||||
remove_list.append(i)
|
||||
|
||||
for i in remove_list:
|
||||
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)
|
||||
|
||||
|
@ -425,12 +464,12 @@ class FONT3D_OT_TestFont(bpy.types.Operator):
|
|||
|
||||
distribution_type = 'DEFAULT'
|
||||
|
||||
t = font3d_data.available_texts.add()
|
||||
text_index = 0
|
||||
text_id = 0
|
||||
for i, tt in enumerate(font3d_data.available_texts):
|
||||
while text_index == tt.text_index:
|
||||
text_index = text_index + 1
|
||||
t.text_index = text_index
|
||||
while text_id == tt.text_id:
|
||||
text_id = text_id + 1
|
||||
t = font3d_data.available_texts.add()
|
||||
t.text_id = text_id
|
||||
|
||||
t.font_name = font_name
|
||||
t.font_face = font_face
|
||||
|
@ -631,15 +670,21 @@ class FONT3D_OT_CreateFontFromObjects(bpy.types.Operator):
|
|||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class HelloWorldPanel(bpy.types.Panel):
|
||||
class FONT3D_PT_RightPropertiesPanel(bpy.types.Panel):
|
||||
"""Creates a Panel in the Object properties window"""
|
||||
bl_label = "Hello World Panel"
|
||||
bl_idname = "OBJECT_PT_hello"
|
||||
bl_label = f"{__name__}"
|
||||
bl_idname = "FONT3D_PT_RightPropertiesPanel"
|
||||
bl_space_type = 'PROPERTIES'
|
||||
bl_region_type = 'WINDOW'
|
||||
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):
|
||||
layout = self.layout
|
||||
scene = context.scene
|
||||
|
@ -648,16 +693,27 @@ class HelloWorldPanel(bpy.types.Panel):
|
|||
|
||||
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.label(text="Hello world!", icon='WORLD_DATA')
|
||||
|
||||
row = layout.row()
|
||||
row.label(text="Active object is: " + obj.name)
|
||||
row = layout.row()
|
||||
row.prop(obj, "location")
|
||||
|
||||
row.label(text="text object is: " + textobject.name)
|
||||
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_SaveFontToFile,
|
||||
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():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
bpy.types.Scene.font3d = bpy.props.PointerProperty(type=FONT3D_settings)
|
||||
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']}")
|
||||
bpy.app.timers.register(execute_queued_functions)
|
||||
|
||||
# would love to properly auto start this, but IT DOES NOT WORK
|
||||
# if load_handler not in bpy.app.handlers.load_post:
|
||||
# bpy.app.handlers.load_post.append(load_handler)
|
||||
# auto start
|
||||
if load_handler not in bpy.app.handlers.load_post:
|
||||
bpy.app.handlers.load_post.append(load_handler)
|
||||
|
||||
# clear available fonts
|
||||
def clear_available_fonts():
|
||||
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(load_available_fonts)
|
||||
|
||||
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:
|
||||
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_data
|
||||
|
|
57
butils.py
57
butils.py
|
@ -89,8 +89,8 @@ def align_rotations_auto_pivot(mask, input_rotations, vectors, factors, local_ma
|
|||
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()
|
||||
# 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
|
||||
|
@ -276,9 +276,6 @@ def find_font_face_object(font_obj, face_name):
|
|||
return None
|
||||
|
||||
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:
|
||||
c.objects.unlink(obj)
|
||||
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["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"]
|
||||
|
@ -367,21 +365,38 @@ def ShowMessageBox(title = "Message Box", icon = 'INFO', message=""):
|
|||
bpy.context.window_manager.popup_menu(draw, title = title, icon = icon)
|
||||
|
||||
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
|
||||
if mom.type != "CURVE":
|
||||
return False
|
||||
|
||||
regenerate = False
|
||||
glyph_objects = []
|
||||
for g in 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) != len(text_properties.glyphs):
|
||||
regenerate = True
|
||||
|
||||
# if we regenerate.... delete objects
|
||||
if regenerate:
|
||||
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)
|
||||
# bpy.ops.object.delete({"selected_objects": glyph_objects})
|
||||
|
||||
text_properties.glyphs.clear()
|
||||
|
||||
#TODO: fix selection with context_override
|
||||
|
@ -394,7 +409,6 @@ def set_text_on_curve(text_properties):
|
|||
glyph_advance = 0
|
||||
is_command = False
|
||||
for i, c in enumerate(text_properties.text):
|
||||
print(f"trying letter ({c})")
|
||||
if c == '\\':
|
||||
is_command = True
|
||||
continue
|
||||
|
@ -407,16 +421,21 @@ def set_text_on_curve(text_properties):
|
|||
continue
|
||||
is_command = False
|
||||
glyph_id = c
|
||||
|
||||
glyph = Font.get_glyph(text_properties.font_name,
|
||||
text_properties.font_face,
|
||||
glyph_id)
|
||||
|
||||
ob = None
|
||||
if regenerate:
|
||||
if glyph == None:
|
||||
self.report({'ERROR'}, f"Glyph not found for {font_name} {font_face} {glyph_id}")
|
||||
continue
|
||||
|
||||
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'
|
||||
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)
|
||||
ob.location = mom.matrix_world @ location
|
||||
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]
|
||||
factors = [1.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)
|
||||
if 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
|
||||
|
||||
glyph_advance = (-1 * glyph.bound_box[0][0] + glyph.bound_box[4][0]) * scalor + text_properties.letter_spacing
|
||||
|
||||
ob.scale = (scalor, scalor, scalor)
|
||||
mom.users_collection[0].objects.link(ob)
|
||||
|
||||
advance = advance + glyph_advance
|
||||
|
||||
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)
|
||||
# selected_objects.append(ob)
|
||||
# selected_objects.append(mom)
|
||||
|
||||
if regenerate:
|
||||
mom.select_set(True)
|
||||
bpy.context.view_layer.objects.active = mom
|
||||
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
|
||||
|
|
Loading…
Reference in a new issue