From 07c38fcdaff306f177afac4f01abfd33bcb4d711 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Wed, 10 Jul 2024 16:34:43 +0200 Subject: [PATCH] regenerate on demand, easier orientation, various --- __init__.py | 173 ++++++++++++++++++++++++++++++++++++++++++++-------- butils.py | 97 ++++++++++++++++------------- 2 files changed, 200 insertions(+), 70 deletions(-) diff --git a/__init__.py b/__init__.py index a478ec4..f0b33d2 100644 --- a/__init__.py +++ b/__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 diff --git a/butils.py b/butils.py index 338350a..1b25829 100644 --- a/butils.py +++ b/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,22 +365,39 @@ 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) - context_override = bpy.context.copy() - context_override["selected_objects"] = list(glyph_objects) - with bpy.context.temp_override(**context_override): - bpy.ops.object.delete() - for g in glyph_objects: - bpy.data.objects.remove(g, do_unlink=True) - # bpy.ops.object.delete({"selected_objects": glyph_objects}) - text_properties.glyphs.clear() + # 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) + + text_properties.glyphs.clear() #TODO: fix selection with context_override previous_selection = bpy.context.selected_objects @@ -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) - if glyph == None: - self.report({'ERROR'}, f"Glyph not found for {font_name} {font_face} {glyph_id}") - continue + 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 = 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': @@ -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 - 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) - - 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') - + 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') return True