diff --git a/.gitignore b/.gitignore index 19bc9a2..220a137 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ # python __pycache__ venv -venv* # vim *.swo diff --git a/README.md b/README.md index c4fa035..ae7cf9a 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,10 @@ / ___ \| |_) | |___ ___) | |_| | /_/ \_\____/ \____|____/|____/ ``` -v0.0.12 +v0.0.7 -Convenience addon to work with 3D typography in Blender and Cinema4D. +Convenience tool to work with 3D typography in Blender and Cinema4D. + +Install as you would normally install an addon. Instructions for development in [CONTRIBUTING,md](./CONTRIBUTING.md). diff --git a/__init__.py b/__init__.py index c4fc090..b866537 100644 --- a/__init__.py +++ b/__init__.py @@ -16,7 +16,7 @@ from .common import Font, utils bl_info = { "name": "ABC3D", "author": "Jakob Schlötter, Studio Pointer*", - "version": (0, 0, 12), + "version": (0, 0, 7), "blender": (4, 1, 0), "location": "VIEW3D", "description": "Convenience addon for 3D fonts", @@ -137,46 +137,16 @@ class ABC3D_glyph_properties(bpy.types.PropertyGroup): def update_callback(self, context): if self.text_id >= 0: - # butils.set_text_on_curve( - # context.scene.abc3d_data.available_texts[self.text_id] - # ) - t = butils.get_text_properties(self.text_id) - if t is not None: - butils.set_text_on_curve(t) - return None - - def alternate_get_callback(self): - return self["alternate"] if "alternate" in self else 0 - - def alternate_set_callback(self, value): - min_value = 0 - new_value = max(value, min_value) - - if self.text_id >= 0: - text_properties = butils.get_text_properties(self.text_id) - max_value = ( - len( - Font.get_glyphs( - text_properties.font_name, - text_properties.face_name, - self.glyph_id, - ) - ) - - 1 + butils.set_text_on_curve( + context.scene.abc3d_data.available_texts[self.text_id] ) - new_value = min(new_value, max_value) - - self["alternate"] = new_value - return None glyph_id: bpy.props.StringProperty(maxlen=1) text_id: bpy.props.IntProperty( - default=-1, - ) + default=-1, + ) alternate: bpy.props.IntProperty( - default=0, # also change in alternate_get_callback - get=alternate_get_callback, - set=alternate_set_callback, + default=-1, update=update_callback, ) glyph_object: bpy.props.PointerProperty(type=bpy.types.Object) @@ -186,7 +156,6 @@ class ABC3D_glyph_properties(bpy.types.PropertyGroup): update=update_callback, ) - class ABC3D_text_properties(bpy.types.PropertyGroup): def font_items_callback(self, context): items = [] @@ -194,6 +163,21 @@ class ABC3D_text_properties(bpy.types.PropertyGroup): items.append((f"{f[0]} {f[1]}", f"{f[0]} {f[1]}", "")) return items + def font_default_callback(self, context): + d = context.scene.abc3d_data + if len(d.available_fonts) > 0: + if len(d.available_fonts) > d.active_text_index: + f = d.available_fonts[d.active_text_index] + return 0 # f"{f.font_name} {f.face_name}" + else: + f = d.available_fonts[0] + return 0 # f"{f.font_name} {f.face_name}" + + if not isinstance(self.font_name, None) and not isinstance(self.face_name, None): + return 0 # f"{self.font_name} {self.face_name}" + else: + return 0 # "" + def glyphs_update_callback(self, context): butils.prepare_text(self.font_name, self.face_name, self.text) butils.set_text_on_curve(self, can_regenerate=True) @@ -275,7 +259,6 @@ class ABC3D_text_properties(bpy.types.PropertyGroup): ) distribution_type: bpy.props.StringProperty() glyphs: bpy.props.CollectionProperty(type=ABC3D_glyph_properties) - actual_text: bpy.props.StringProperty() class ABC3D_data(bpy.types.PropertyGroup): @@ -294,33 +277,22 @@ class ABC3D_data(bpy.types.PropertyGroup): available_texts: bpy.props.CollectionProperty( type=ABC3D_text_properties, name="Available texts" ) - texts: bpy.props.CollectionProperty(type=ABC3D_text_properties, name="texts") def active_text_index_update(self, context): - lock_depsgraph_updates() if self.active_text_index != -1: - text_properties = butils.get_text_properties_by_index( - self.active_text_index, context.scene - ) - if text_properties is not None: - o = text_properties.text_object - # active_text_index changed. so let's update the selection - # check if it is already selected - # or perhaps one of the glyphs - if os is not None and not butils.is_or_has_parent( - context.active_object, o - ): - # if ( - # o is not None - # and not o.select_get() - # and not len([c for c in o.children if c.select_get()]) > 0 - # ): - bpy.ops.object.select_all(action="DESELECT") - o.select_set(True) - context.view_layer.objects.active = o - unlock_depsgraph_updates() - # else: - # print("already selected") + o = self.available_texts[self.active_text_index].text_object + # active_text_index changed. so let's update the selection + # check if it is already selected + # or perhaps one of the glyphs + if ( + not o.select_get() + and not len([c for c in o.children if c.select_get()]) > 0 + ): + bpy.ops.object.select_all(action="DESELECT") + o.select_set(True) + bpy.context.view_layer.objects.active = o + # else: + # print("already selected") active_text_index: bpy.props.IntProperty(update=active_text_index_update) @@ -369,7 +341,7 @@ class ABC3D_UL_texts(bpy.types.UIList): class ABC3D_PT_Panel(bpy.types.Panel): - bl_label = f"{utils.prefix()} Panel" + bl_label = f"{__name__} panel" bl_category = "ABC3D" bl_space_type = "VIEW_3D" bl_region_type = "UI" @@ -377,9 +349,6 @@ class ABC3D_PT_Panel(bpy.types.Panel): def draw(self, context): layout = self.layout - row = layout.row() - row.label(text=f"{utils.prefix()} v{utils.get_version_string()}") - icon = "NONE" if len(context.scene.abc3d_data.available_fonts) == 0: icon = "ERROR" @@ -419,65 +388,55 @@ class ABC3D_PT_FontList(bpy.types.Panel): abc3d_data, "active_font_index", ) - if ( - abc3d_data.active_font_index >= 0 - and len(abc3d_data.available_fonts) > abc3d_data.active_font_index - ): + if abc3d_data.active_font_index >= 0: available_font = abc3d_data.available_fonts[abc3d_data.active_font_index] font_name = available_font.font_name face_name = available_font.face_name - face: Font.FontFace = Font.get_font_face(font_name, face_name) - if face is not None: - available_glyphs = face.glyphs_in_fontfile - loaded_glyphs = sorted(face.loaded_glyphs) - box = layout.box() - box.row().label(text=f"Font Name: {font_name}") - box.row().label(text=f"Face Name: {face_name}") - n = 16 - n_rows = int(len(available_glyphs) / n) - box.row().label(text="Glyphs:") - subbox = box.box() - for i in range(0, n_rows + 1): - text = "".join( - [ - f"{u}" - for ui, u in enumerate(available_glyphs) - if ui < (i + 1) * n and ui >= i * n - ] - ) - scale_y = 0.5 - row = subbox.row() - row.scale_y = scale_y - row.alignment = "CENTER" - row.label(text=text) - n_rows = int(len(loaded_glyphs) / n) - box.row().label(text="Loaded/Used Glyphs:") - subbox = box.box() - for i in range(0, n_rows + 1): - text = "".join( - [ - f"{u}" - for ui, u in enumerate(loaded_glyphs) - if ui < (i + 1) * n and ui >= i * n - ] - ) - scale_y = 0.5 - row = subbox.row() - row.scale_y = scale_y - row.label(text=text) - row = layout.row() - oper_lf = row.operator( - f"{__name__}.load_font", text="Load all glyphs in memory" + available_glyphs = sorted( + Font.fonts[font_name].faces[face_name].glyphs_in_fontfile + ) + loaded_glyphs = sorted(Font.fonts[font_name].faces[face_name].loaded_glyphs) + box = layout.box() + box.row().label(text=f"Font Name: {font_name}") + box.row().label(text=f"Face Name: {face_name}") + n = 16 + n_rows = int(len(available_glyphs) / n) + box.row().label(text="Glyphs:") + subbox = box.box() + for i in range(0, n_rows + 1): + text = "".join( + [ + f"{u}" + for ui, u in enumerate(available_glyphs) + if ui < (i + 1) * n and ui >= i * n + ] ) - oper_lf.font_name = font_name - oper_lf.face_name = face_name - box = layout.box() - row = box.row() - row.label(text="File and Memory optimization") - row = box.row() - row.operator(f"{__name__}.refresh_fonts", text="Refresh font list from disk") - row = box.row() - row.operator(f"{__name__}.unload_unused_glyphs", text="Unload unused glyphs") + scale_y = 0.5 + row = subbox.row() + row.scale_y = scale_y + row.alignment = "CENTER" + row.label(text=text) + n_rows = int(len(loaded_glyphs) / n) + box.row().label(text="Loaded/Used Glyphs:") + subbox = box.box() + for i in range(0, n_rows + 1): + text = "".join( + [ + f"{u}" + for ui, u in enumerate(loaded_glyphs) + if ui < (i + 1) * n and ui >= i * n + ] + ) + scale_y = 0.5 + row = subbox.row() + row.scale_y = scale_y + row.label(text=text) + row = layout.row() + oper_lf = row.operator( + f"{__name__}.load_font", text="Load all glyphs in memory" + ) + oper_lf.font_name = font_name + oper_lf.face_name = face_name class ABC3D_PT_TextPlacement(bpy.types.Panel): @@ -491,7 +450,10 @@ class ABC3D_PT_TextPlacement(bpy.types.Panel): @classmethod def poll(self, context): - if context.active_object is not None and context.active_object.type == "CURVE": + if ( + type(context.active_object) != type(None) + and context.active_object.type == "CURVE" + ): self.can_place = True else: self.can_place = False @@ -520,6 +482,99 @@ class ABC3D_PT_TextManagement(bpy.types.Panel): bl_region_type = "UI" bl_options = {"DEFAULT_CLOSED"} + # TODO: perhaps this should be done in a periodic timer + @classmethod + def poll(self, context): + scene = context.scene + abc3d_data = scene.abc3d_data + # TODO: update available_texts + + def update(): + if bpy.context.screen.is_animation_playing: + return + active_text_index = -1 + remove_list = [] + for i, t in enumerate(abc3d_data.available_texts): + if type(t.text_object) == type(None): + remove_list.append(i) + continue + remove_me = True + for c in t.text_object.children: + if ( + len(c.users_collection) > 0 + and not isinstance(c.get(f"{utils.prefix()}_linked_textobject"), None) + and c.get(f"{utils.prefix()}_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: + if type(abc3d_data.available_texts[i].text_object) != type(None): + mom = abc3d_data.available_texts[i].text_object + + def delif(o, p): + if p in o: + del o[p] + + delif(mom, f"{utils.prefix()}_linked_textobject") + delif(mom, f"{utils.prefix()}_font_name") + delif(mom, f"{utils.prefix()}_face_name") + delif(mom, f"{utils.prefix()}_font_size") + delif(mom, f"{utils.prefix()}_letter_spacing") + delif(mom, f"{utils.prefix()}_orientation") + delif(mom, f"{utils.prefix()}_translation") + delif(mom, f"{utils.prefix()}_offset") + abc3d_data.available_texts.remove(i) + + for i, t in enumerate(abc3d_data.available_texts): + if context.active_object == t.text_object: + active_text_index = i + if ( + hasattr(context.active_object, "parent") + and context.active_object.parent == t.text_object + ): + active_text_index = i + + if active_text_index != abc3d_data.active_text_index: + abc3d_data.active_text_index = active_text_index + + # butils.run_in_main_thread(update) + + return True + def draw(self, context): layout = self.layout wm = context.window_manager @@ -589,7 +644,6 @@ class ABC3D_PG_FontCreation(bpy.types.PropertyGroup): update=naming_glyph_id_update_callback, ) - class ABC3D_OT_NamingHelper(bpy.types.Operator): bl_label = "Font Creation Naming Helper Apply To Active Object" bl_idname = f"{__name__}.apply_naming_helper" @@ -627,10 +681,7 @@ class ABC3D_PT_NamingHelper(bpy.types.Panel): box.row().prop(abc3d_font_creation, "face_name") box.label(text="Glyph Output Name") box.row().prop(abc3d_font_creation, "naming_glyph_name") - box.row().operator( - f"{__name__}.apply_naming_helper", text="Apply name to active object" - ) - + box.row().operator(f"{__name__}.apply_naming_helper", text="Apply name to active object") class ABC3D_PT_FontCreation(bpy.types.Panel): bl_label = "Font Creation" @@ -669,10 +720,7 @@ class ABC3D_PT_FontCreation(bpy.types.Panel): f"{__name__}.temporaryhelper", text="Debug Function Do Not Use" ) box.label(text="origin points") - box.row().operator( - f"{__name__}.align_origins_to_active_object", - text="Align origins to Active Object", - ) + box.row().operator(f"{__name__}.align_origins_to_active_object", text="Align origins to Active Object") # box.row().operator(f"{__name__}.align_origins_to_metrics", text="Align origins to Metrics (left)") # box.row().operator(f"{__name__}.fix_objects_metrics_origins", text="Fix objects metrics origins") @@ -688,48 +736,75 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): # and bpy.context.object.select_get(): a_o = bpy.context.active_object if a_o is not None: - # if f"{utils.prefix()}_text_id" in a_o: - # text_id = a_o[f"{utils.prefix()}_text_id"] - # return butils.get_text_properties(text_id) - # # elif a_o.parent is not None and f"{utils.prefix()}_text_id" in a_o.parent: - # # text_id = a_o.parent[f"{utils.prefix()}_text_id"] - # # return butils.get_text_properties(text_id) - # else: - for t in bpy.context.scene.abc3d_data.available_texts: - if butils.is_or_has_parent( - bpy.context.active_object, t.text_object, max_depth=4 - ): - return t + if f"{utils.prefix()}_linked_textobject" in a_o: + text_index = a_o[f"{utils.prefix()}_linked_textobject"] + return bpy.context.scene.abc3d_data.available_texts[text_index] + elif a_o.parent is not None and f"{utils.prefix()}_linked_textobject" in a_o.parent: + text_index = a_o.parent[f"{utils.prefix()}_linked_textobject"] + return bpy.context.scene.abc3d_data.available_texts[text_index] + else: + for t in bpy.context.scene.abc3d_data.available_texts: + if butils.is_or_has_parent(bpy.context.active_object, t.text_object, max_depth=4): + return t return None - def get_active_glyph_properties(self, text_properties): - if text_properties is None: - return None + # NOTE: HERE + def get_active_glyph_properties(self): a_o = bpy.context.active_object if a_o is not None: - if f"{utils.prefix()}_glyph_index" in a_o: + if (f"{utils.prefix()}_linked_textobject" in a_o + and f"{utils.prefix()}_glyph_index" in a_o): + text_index = a_o[f"{utils.prefix()}_linked_textobject"] glyph_index = a_o[f"{utils.prefix()}_glyph_index"] - if len(text_properties.glyphs) <= glyph_index: - return None - return text_properties.glyphs[glyph_index] + return bpy.context.scene.abc3d_data.available_texts[text_index].glyphs[glyph_index] else: - for g in text_properties.glyphs: - if butils.is_or_has_parent(a_o, g.glyph_object, max_depth=4): - return g + for t in bpy.context.scene.abc3d_data.available_texts: + if butils.is_or_has_parent(a_o, t.text_object, if_is_parent=False, max_depth=4): + for g in t.glyphs: + if butils.is_or_has_parent(a_o, g.glyph_object, max_depth=4): + return g return None + + # def font_items_callback(self, context): + # items = [] + # fonts = Font.get_loaded_fonts_and_faces() + # for f in fonts: + # items.append((f"{f[0]} {f[1]}", f"{f[0]} {f[1]}", "")) + # return items + + # def font_default_callback(self, context): + # t = self.get_active_text_properties(self) + # if type(t) != type(None): + # return f"{t.font_name} {t.face_name}" + # else: + # return None + + # def font_update_callback(self, context): + # font_name, face_name = self.font.split(" ") + # t = self.get_active_text_properties(self) + # t.font_name = font_name + # t.face_name = face_name + # butils.set_text_on_curve(t) + + # font: bpy.props.EnumProperty( + # items=font_items_callback, + # default=font_default_callback, + # update=font_update_callback, + # ) + @classmethod def poll(self, context): - try: - return self.get_active_text_properties(self) is not None - except IndexError: - return False + return self.get_active_text_properties(self) is not None def draw(self, context): layout = self.layout + wm = context.window_manager + scene = context.scene + abc3d_data = scene.abc3d_data props = self.get_active_text_properties() - glyph_props = self.get_active_glyph_properties(props) + glyph_props = self.get_active_glyph_properties() if props is None or props.text_object is None: # this should not happen @@ -741,22 +816,6 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): layout.label(text="props.text_object is none") return - # TODO: put this at a better place - # here we set the font if it is not correct - # this is a fix for a UI glitch, perhaps it could be fixed - # rather where it is not set properly - # if ( - # butils.get_key("font_name") in props.text_object - # and butils.get_key("face_name") in props.text_object - # ): - # font = f"{props.text_object[butils.get_key('font_name')]} {props.text_object[butils.get_key('face_name')]}" - # if font != props.font: - # - # def setfont(): - # props.font = font - # - # butils.run_in_main_thread(setfont) - # layout.label(text=f"Mom: {props.text_object.name}") layout.row().prop(props, "font") layout.row().prop(props, "text") @@ -773,54 +832,10 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): if glyph_props is None: return box = layout.box() - box.label(text=f"selected character: {glyph_props.glyph_id}") + box.label(text=f"{glyph_props.glyph_id}") box.row().prop(glyph_props, "letter_spacing") - # if True: - # font_name = props.font_name - # face_name = props.face_name - # glyph_id = glyph_props.glyph_id - # glyphs_n = len(Font.get_glyphs(font_name, face_name, glyph_id)) - # glyph_props.alternate.hard_min = -1 - # glyph_props.alternate.hard_max = glyphs_n - 1 - n_alternates = len( - Font.get_glyphs( - props.font_name, - props.face_name, - glyph_props.glyph_id, - ) - ) - if n_alternates > 1: - box.row().prop(glyph_props, "alternate", text=f"alternate ({n_alternates})") - # if glyph_props.glyph_object.preview is not None: - # box.row().template_preview(glyph_props.glyph_object.preview.icon_id) -class ABC3D_OT_RefreshAvailableFonts(bpy.types.Operator): - """Refreshes available font list from disk. - This also removes all fonts which are not saved in the asset directory. - Can be useful when creating fonts or manually installing fonts.""" - - bl_idname = f"{__name__}.refresh_fonts" - bl_label = "Refresh Available Fonts" - bl_options = {"REGISTER", "UNDO"} - - def execute(self, context): - refresh_fonts() - return {"FINISHED"} - - -class ABC3D_OT_UnloadUnusedGlyphs(bpy.types.Operator): - """Unload all glyphs which are not actively used in this project from memory. - They will still be normally loaded when you use them again.""" - - bl_idname = f"{__name__}.unload_unused_glyphs" - bl_label = "Unload Unused Glyphs" - bl_options = {"REGISTER", "UNDO"} - - def execute(self, context): - butils.unload_unused_glyphs() - return {"FINISHED"} - class ABC3D_OT_InstallFont(bpy.types.Operator): """Install or load Fontfile from path above. @@ -980,6 +995,7 @@ class ABC3D_OT_LoadInstalledFonts(bpy.types.Operator): return context.window_manager.invoke_props_dialog(self) def execute(self, context): + print("EXECUTE LOAD INSTALLED FONTS") scene = bpy.context.scene if self.load_into_memory: @@ -1004,18 +1020,7 @@ class ABC3D_OT_LoadFont(bpy.types.Operator): face_name: bpy.props.StringProperty() def execute(self, context): - face: Font.FontFace = Font.get_font_face(self.font_name, self.face_name) - if face is None: - butils.ShowMessageBox( - f"{utils.prefix()} Load Font", - icon="ERROR", - message=[ - "Could not load font, sorry!", - f"{self.font_name=} {self.face_name=}", - ], - ) - return {"CANCELLED"} - filepaths = face.filepaths + filepaths = Font.fonts[self.font_name].faces[self.face_name].filepaths for f in filepaths: butils.load_font_from_filepath(f) return {"FINISHED"} @@ -1077,7 +1082,6 @@ class ABC3D_OT_AlignMetrics(bpy.types.Operator): butils.align_metrics_of_objects(objects) return {"FINISHED"} - class ABC3D_OT_AlignOriginsToActiveObject(bpy.types.Operator): """Align origins of selected objects to origin of active object on one axis.""" @@ -1085,67 +1089,65 @@ class ABC3D_OT_AlignOriginsToActiveObject(bpy.types.Operator): bl_label = "Align origins to Active Object" bl_options = {"REGISTER", "UNDO"} - enum_axis = (("0", "X", ""), ("1", "Y", ""), ("2", "Z", "")) - axis: bpy.props.EnumProperty(items=enum_axis, default="2") + enum_axis = (('0','X',''),('1','Y',''),('2','Z','')) + axis: bpy.props.EnumProperty(items = enum_axis, default='2') def execute(self, context): objects = bpy.context.selected_objects butils.align_origins_to_active_object(objects, int(self.axis)) return {"FINISHED"} - # class ABC3D_OT_AlignOriginsToMetrics(bpy.types.Operator): -# """Align origins of selected objects to their metrics left border. + # """Align origins of selected objects to their metrics left border. -# Be aware that shifting the origin will also shift the pivot point around which an object rotates. + # Be aware that shifting the origin will also shift the pivot point around which an object rotates. -# If an object does not have metrics, it will be ignored.""" + # If an object does not have metrics, it will be ignored.""" -# bl_idname = f"{__name__}.align_origins_to_metrics" -# bl_label = "Align origins to metrics metrics" -# bl_options = {"REGISTER", "UNDO"} + # bl_idname = f"{__name__}.align_origins_to_metrics" + # bl_label = "Align origins to metrics metrics" + # bl_options = {"REGISTER", "UNDO"} -# ignore_warning: bpy.props.BoolProperty( -# name="Do not warn in the future", -# description="Do not warn in the future", -# default=False, -# ) + # ignore_warning: bpy.props.BoolProperty( + # name="Do not warn in the future", + # description="Do not warn in the future", + # default=False, + # ) -# def draw(self, context): -# layout = self.layout -# layout.row().label(text="Warning!") -# layout.row().label(text="This also shifts the pivot point around which the glyph rotates.") -# layout.row().label(text="This may not be what you want.") -# layout.row().label(text="Glyph advance derives from metrics boundaries, not origin points.") -# layout.row().label(text="If you are sure about what you're doing, please continue.") -# layout.row().prop(self, "ignore_warning") + # def draw(self, context): + # layout = self.layout + # layout.row().label(text="Warning!") + # layout.row().label(text="This also shifts the pivot point around which the glyph rotates.") + # layout.row().label(text="This may not be what you want.") + # layout.row().label(text="Glyph advance derives from metrics boundaries, not origin points.") + # layout.row().label(text="If you are sure about what you're doing, please continue.") + # layout.row().prop(self, "ignore_warning") -# def invoke(self, context, event): -# if not self.ignore_warning: -# wm = context.window_manager -# return wm.invoke_props_dialog(self) -# return self.execute(context) + # def invoke(self, context, event): + # if not self.ignore_warning: + # wm = context.window_manager + # return wm.invoke_props_dialog(self) + # return self.execute(context) -# def execute(self, context): -# objects = bpy.context.selected_objects -# butils.align_origins_to_metrics(objects) -# butils.fix_objects_metrics_origins(objects) -# return {"FINISHED"} + # def execute(self, context): + # objects = bpy.context.selected_objects + # butils.align_origins_to_metrics(objects) + # butils.fix_objects_metrics_origins(objects) + # return {"FINISHED"} # class ABC3D_OT_FixObjectsMetricsOrigins(bpy.types.Operator): -# """Align metrics origins of selected objects to their metrics bounding box. + # """Align metrics origins of selected objects to their metrics bounding box. -# If an object does not have metrics, it will be ignored.""" + # If an object does not have metrics, it will be ignored.""" -# bl_idname = f"{__name__}.fix_objects_metrics_origins" -# bl_label = "Fix metrics origin of all selected objects" -# bl_options = {"REGISTER", "UNDO"} - -# def execute(self, context): -# objects = bpy.context.selected_objects -# butils.fix_objects_metrics_origins(objects) -# return {"FINISHED"} + # bl_idname = f"{__name__}.fix_objects_metrics_origins" + # bl_label = "Fix metrics origin of all selected objects" + # bl_options = {"REGISTER", "UNDO"} + # def execute(self, context): + # objects = bpy.context.selected_objects + # butils.fix_objects_metrics_origins(objects) + # return {"FINISHED"} class ABC3D_OT_TemporaryHelper(bpy.types.Operator): """Temporary Helper ABC3D\nThis could do anything.\nIt's just there to make random functions available for testing.""" @@ -1188,11 +1190,6 @@ class ABC3D_OT_RemoveText(bpy.types.Operator): description="Remove both ABC3D text functionality and the objects/meshes", default=True, ) - remove_custom_properties: bpy.props.BoolProperty( - name="Remove Custom Properties", - description="Remove ABC3D custom properties of objects", - default=True, - ) def invoke(self, context, event): wm = context.window_manager @@ -1200,7 +1197,6 @@ class ABC3D_OT_RemoveText(bpy.types.Operator): def execute(self, context): abc3d_data = context.scene.abc3d_data - lock_depsgraph_updates() if abc3d_data.active_text_index < 0: butils.ShowMessageBox( title="No text selected", @@ -1210,33 +1206,30 @@ class ABC3D_OT_RemoveText(bpy.types.Operator): return {"CANCELLED"} i = abc3d_data.active_text_index - if abc3d_data.available_texts[i].text_object is not None: + if type(abc3d_data.available_texts[i].text_object) != type(None): mom = abc3d_data.available_texts[i].text_object - if self.remove_custom_properties: + def delif(o, p): + if p in o: + del o[p] - def delif(o, p): - if p in o: - del o[p] - - delif(mom, f"{utils.prefix()}_type") - delif(mom, f"{utils.prefix()}_text_id") - delif(mom, f"{utils.prefix()}_font_name") - delif(mom, f"{utils.prefix()}_face_name") - delif(mom, f"{utils.prefix()}_font_size") - delif(mom, f"{utils.prefix()}_letter_spacing") - delif(mom, f"{utils.prefix()}_orientation") - delif(mom, f"{utils.prefix()}_translation") - delif(mom, f"{utils.prefix()}_offset") + delif(mom, f"{utils.prefix()}_type") + delif(mom, f"{utils.prefix()}_linked_textobject") + delif(mom, f"{utils.prefix()}_font_name") + delif(mom, f"{utils.prefix()}_face_name") + delif(mom, f"{utils.prefix()}_font_size") + delif(mom, f"{utils.prefix()}_letter_spacing") + delif(mom, f"{utils.prefix()}_orientation") + delif(mom, f"{utils.prefix()}_translation") + delif(mom, f"{utils.prefix()}_offset") if self.remove_objects: remove_list = [] for g in abc3d_data.available_texts[i].glyphs: - if g is not None: + if type(g) != type(None): remove_list.append(g.glyph_object) butils.simply_delete_objects(remove_list) abc3d_data.available_texts.remove(i) - unlock_depsgraph_updates() return {"FINISHED"} @@ -1320,7 +1313,10 @@ class ABC3D_OT_PlaceText(bpy.types.Operator): distribution_type = "DEFAULT" - text_id = butils.find_free_text_id() + text_id = 0 + for i, tt in enumerate(abc3d_data.available_texts): + while text_id == tt.text_id: + text_id = text_id + 1 t = abc3d_data.available_texts.add() # If you wish to set a value and not fire an update, set the id property. # A property defined via bpy.props for example ob.prop is stored as ob["prop"] once set to non default. @@ -1362,8 +1358,7 @@ class ABC3D_OT_PlaceText(bpy.types.Operator): class ABC3D_OT_ToggleABC3DCollection(bpy.types.Operator): """Toggle ABC3D Collection. - This will show the Fonts and Glyphs currently loaded by ABC3D. Useful for font creation, debugging and inspection. - """ + This will show the Fonts and Glyphs currently loaded by ABC3D. Useful for font creation, debugging and inspection.""" bl_idname = f"{__name__}.toggle_abc3d_collection" bl_label = "Toggle Collection visibility" @@ -1395,9 +1390,6 @@ class ABC3D_OT_SaveFontToFile(bpy.types.Operator): bl_label = "Save Font" bl_options = {"REGISTER", "UNDO"} - can_execute: bpy.props.BoolProperty(default=True) - create_output_directory: bpy.props.BoolProperty(default=False) - def invoke(self, context, event): wm = context.window_manager preferences = getPreferences(context) @@ -1421,99 +1413,30 @@ class ABC3D_OT_SaveFontToFile(bpy.types.Operator): available_font = abc3d_data.available_fonts[abc3d_data.active_font_index] font_name = available_font.font_name face_name = available_font.face_name - face: Font.FontFace = Font.get_font_face(font_name, face_name) - if face is not None: - loaded_glyphs = sorted(face.loaded_glyphs) - n = 16 - n_rows = int(len(loaded_glyphs) / n) - box = layout.box() - box.row().label(text="Glyphs to be exported:") - subbox = box.box() - for i in range(0, n_rows + 1): - text = "".join( - [ - f"{u}" - for ui, u in enumerate(loaded_glyphs) - if ui < (i + 1) * n and ui >= i * n - ] - ) - scale_y = 0.5 - row = subbox.row() - row.scale_y = scale_y - row.label(text=text) - row = layout.row() - export_dir = butils.bpy_to_abspath(abc3d_data.export_dir) - if os.access(export_dir, os.W_OK): - self.can_execute = True - elif os.path.exists(export_dir): - self.can_execute = False - row.alert = True - row.label(text="Export directory exists but is not writable") - row = layout.row() - row.alert = True - row.label(text="Please select another directory") - row = layout.row() - row.alert = True - elif not utils.can_create_path( - export_dir - ): # does not exist and cannot be created - self.can_execute = False - row.alert = True - row.label(text="Directory does not exist and cannot be created") - row = layout.row() - row.alert = True - row.label(text="Please select another directory") - row = layout.row() - row.alert = True - elif utils.can_create_path(export_dir): # does not exist and can be created - self.can_execute = True - row.label(text="Directory does not exist") - row = layout.row() - row.label(text="But can and will be created on export") - row = layout.row() - else: - self.can_execute = False - row.alert = True - row.label(text="Please select another directory") - row = layout.row() - row.alert = True - - row.prop(abc3d_data, "export_dir") - else: - print( - f"{utils.prefix()}::save_font_to_file ERROR {face=} {font_name=} {face_name=}" + loaded_glyphs = sorted(Font.fonts[font_name].faces[face_name].loaded_glyphs) + n = 16 + n_rows = int(len(loaded_glyphs) / n) + box = layout.box() + box.row().label(text="Glyphs to be exported:") + subbox = box.box() + for i in range(0, n_rows + 1): + text = "".join( + [ + f"{u}" + for ui, u in enumerate(loaded_glyphs) + if ui < (i + 1) * n and ui >= i * n + ] ) - print(f"{utils.prefix()} {Font.fonts=}") + scale_y = 0.5 + row = subbox.row() + row.scale_y = scale_y + row.label(text=text) + layout.prop(abc3d_data, "export_dir") def execute(self, context): global shared scene = bpy.context.scene abc3d_data = scene.abc3d_data - if not self.can_execute: - butils.ShowMessageBox( - "Cannot export font", - "ERROR", - [ - f"export directory '{abc3d_data.export_dir}' does not exist or is not writable", - "try setting another path", - ], - ) - return {"CANCELLED"} - - if not os.path.exists(butils.bpy_to_abspath(abc3d_data.export_dir)): - path = butils.bpy_to_abspath(abc3d_data.export_dir) - if utils.can_create_path(path): - os.makedirs(path, exist_ok=True) - else: - butils.ShowMessageBox( - "Cannot export font", - "ERROR", - [ - f"export directory '{abc3d_data.export_dir}' does not exist and cannot be created", - "try setting another path", - ], - ) - return {"CANCELLED"} fontcollection = bpy.data.collections.get("ABC3D") @@ -1593,12 +1516,7 @@ class ABC3D_OT_SaveFontToFile(bpy.types.Operator): use_selection=True, use_active_scene=True, ) - - def delete_scene(): - bpy.ops.scene.delete() - return None - - bpy.app.timers.register(lambda: delete_scene(), first_interval=1) + bpy.app.timers.register(lambda: bpy.ops.scene.delete(), first_interval=1) # bpy.ops.scene.delete() # restore() @@ -1611,7 +1529,7 @@ class ABC3D_OT_SaveFontToFile(bpy.types.Operator): return None bpy.app.timers.register(lambda: remove_faces(), first_interval=2) - self.report({"INFO"}, f"{utils.prefix()}::save_font_to_file done") + self.report({"INFO"}, "did it") return {"FINISHED"} @@ -1648,10 +1566,9 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): def do_autodetect_names(self, name: str): ifxsplit = name.split("_") if len(ifxsplit) < 4: - print( - f"{utils.prefix()}::CreateFontFromObjects: name could not be autodetected {name}" - ) - print(f"{utils.prefix()}::CreateFontFromObjects: split: {ifxsplit=}") + print(f"name could not be autodetected {name}") + print("split:") + print(ifxsplit) return self.font_name, self.face_name detected_font_name = f"{ifxsplit[1]}_{ifxsplit[2]}" detected_face_name = ifxsplit[3] @@ -1667,9 +1584,7 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): row.prop(self, "autodetect_names") first_object_name = context.selected_objects[-1].name if self.autodetect_names: - self.font_name, self.face_name = self.do_autodetect_names( - first_object_name - ) + self.font_name, self.face_name = self.do_autodetect_names(first_object_name) if self.autodetect_names: scale_y = 0.5 row = layout.row() @@ -1726,11 +1641,9 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): row.label(text=f"{k} → {Font.known_misspellings[k]}{character}") def execute(self, context): - print(f"{utils.prefix()}::CreateFontFromObjects: executing {self.bl_idname}") + print(f"executing {self.bl_idname}") if len(context.selected_objects) == 0: - print( - f"{utils.prefix()}::CreateFontFromObjects: cancelled {self.bl_idname} - no objects selected" - ) + print(f"cancelled {self.bl_idname} - no objects selected") return {"CANCELLED"} global shared scene = bpy.context.scene @@ -1744,10 +1657,13 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): font_name = self.font_name face_name = self.face_name + # TODO: do not clear + # abc3d_data.available_fonts.clear() + # Font.fonts = {} currentObjects = [] for o in context.selected_objects: if o.name not in currentObjects: - print(f"{utils.prefix()}::CreateFontFromObjects: processing {o.name}") + print(f"processing {o.name}") process_object = True if self.autodetect_names: font_name, face_name = self.do_autodetect_names(o.name) @@ -1784,9 +1700,7 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): f.face_name = face_name else: - print( - f"{utils.prefix()}::CreateFontFromObjects: import warning: did not understand glyph {name}" - ) + print(f"import warning: did not understand glyph {name}") self.report({"INFO"}, f"did not understand glyph {name}") return {"FINISHED"} @@ -1833,8 +1747,6 @@ classes = ( ABC3D_PT_NamingHelper, ABC3D_PT_TextPropertiesPanel, ABC3D_OT_OpenAssetDirectory, - ABC3D_OT_RefreshAvailableFonts, - ABC3D_OT_UnloadUnusedGlyphs, ABC3D_OT_LoadInstalledFonts, ABC3D_OT_LoadFont, ABC3D_OT_AddDefaultMetrics, @@ -1874,61 +1786,19 @@ def compare_text_object_with_object(t, o, strict=False): return True -def link_text_object_with_new_text_properties(text_object, scene=None): - lock_depsgraph_updates() - butils.link_text_object_with_new_text_properties(text_object, scene) - unlock_depsgraph_updates() - - -def determine_active_text_index_from_selection(): - if bpy.context.active_object is None: - return -1 - for text_index, text_properties in enumerate( - bpy.context.scene.abc3d_data.available_texts - ): - if butils.is_text_object_legit(text_properties.text_object): - if butils.is_or_has_parent( - bpy.context.active_object, text_properties.text_object - ): - return text_index - return -1 - - -def update_active_text_index(): - text_index = determine_active_text_index_from_selection() - if text_index != bpy.context.scene.abc3d_data.active_text_index: - bpy.context.scene.abc3d_data.active_text_index = text_index - - def detect_text(): - lock_depsgraph_updates() scene = bpy.context.scene abc3d_data = scene.abc3d_data - required_keys = [ - "type", - "text_id", - "font_name", - "face_name", - "text", - ] - objects = scene.objects - for o in objects: - valid = True - for key in required_keys: - if butils.get_key(key) not in o: - valid = False - break - if not valid: - continue - if o[butils.get_key("type")] == "textobject": - current_text_id = int(o[butils.get_key("text_id")]) - text_properties = butils.get_text_properties(current_text_id) - if text_properties is not None and text_properties.text_object == o: - # all good - pass - else: - butils.link_text_object_with_new_text_properties(o, scene) - unlock_depsgraph_updates() + for o in scene.objects: + if o[f"{utils.prefix()}_type"] == "textobject": + linked_textobject = int(o[f"{utils.prefix()}_linked_textobject"]) + if ( + len(abc3d_data.available_texts) > linked_textobject + and abc3d_data.available_texts[linked_textobject].text_object == o + ): + t = abc3d_data.available_texts[linked_textobject] + a = test_availability(o["font_name"], o["face_name"], o["text"]) + butils.transfer_blender_object_to_text_properties(o, t) def load_used_glyphs(): @@ -1936,7 +1806,7 @@ def load_used_glyphs(): abc3d_data = scene.abc3d_data for t in abc3d_data.available_texts: a = Font.test_availability(t.font_name, t.face_name, t.text) - if isinstance(a, int): + if type(a) == type(int()): if a == Font.MISSING_FONT: butils.ShowMessageBox( "Missing Font", @@ -1953,24 +1823,9 @@ def load_used_glyphs(): "Do you have it installed?", ], ) - elif len(a.unloaded) > 0: - for fp in a.filepaths: - butils.load_font_from_filepath(fp, a.unloaded) - - -def refresh_fonts(): - fontcollection: bpy_types.Collection = bpy.data.collections.get("ABC3D") - if fontcollection is not None: - objs = [o for o in fontcollection.objects if o.parent == None] - butils.completely_delete_objects(objs) - butils.run_in_main_thread(Font.fonts.clear) - butils.run_in_main_thread(butils.clear_available_fonts) - butils.run_in_main_thread(butils.register_installed_fonts) - butils.run_in_main_thread(butils.update_available_fonts) - butils.run_in_main_thread(load_used_glyphs) - butils.run_in_main_thread(butils.update_types) - butils.run_in_main_thread(detect_text) - butils.run_in_main_thread(butils.unload_unused_glyphs) + elif len(a["maybe"]) > 0: + for fp in a["filepaths"]: + butils.load_font_from_filepath(fp, a["maybe"]) @persistent @@ -1995,26 +1850,20 @@ def on_frame_changed(self, dummy): butils.set_text_on_curve(t) -depsgraph_updates_locked = 0 +depsgraph_updates_locked = False def unlock_depsgraph_updates(): global depsgraph_updates_locked - depsgraph_updates_locked -= 1 + depsgraph_updates_locked = False -def lock_depsgraph_updates(auto_unlock_s=-1): +def lock_depsgraph_updates(): global depsgraph_updates_locked - depsgraph_updates_locked += 1 - if auto_unlock_s >= 0: - if bpy.app.timers.is_registered(unlock_depsgraph_updates): - bpy.app.timers.unregister(unlock_depsgraph_updates) - bpy.app.timers.register(unlock_depsgraph_updates, first_interval=auto_unlock_s) - - -def are_depsgraph_updates_locked(): - global depsgraph_updates_locked - return depsgraph_updates_locked > 0 + depsgraph_updates_locked = True + if bpy.app.timers.is_registered(unlock_depsgraph_updates): + bpy.app.timers.unregister(unlock_depsgraph_updates) + bpy.app.timers.register(unlock_depsgraph_updates, first_interval=1) import time @@ -2022,37 +1871,30 @@ import time @persistent def on_depsgraph_update(scene, depsgraph): - if not bpy.context.mode.startswith("EDIT") and not are_depsgraph_updates_locked(): - lock_depsgraph_updates(auto_unlock_s=-1) + global depsgraph_updates_locked + if not bpy.context.mode.startswith("EDIT") and not depsgraph_updates_locked: for u in depsgraph.updates: if ( - butils.get_key("text_id") in u.id.keys() - and butils.get_key("type") in u.id.keys() - and u.id[butils.get_key("type")] == "textobject" + f"{utils.prefix()}_linked_textobject" in u.id.keys() + and f"{utils.prefix()}_type" in u.id.keys() + and u.id[f"{utils.prefix()}_type"] == "textobject" ): - text_id = u.id[butils.get_key("text_id")] - # if u.is_updated_geometry: - text_properties = butils.get_text_properties(text_id) - if text_properties is not None: - if text_properties.text_object == u.id.original: - # nothing to do - try: - butils.set_text_on_curve(text_properties) - except: - pass - pass - elif butils.is_text_object_legit(u.id.original): - # must be duplicate - link_text_object_with_new_text_properties(u.id.original, scene) - elif ( - butils.is_text_object_legit(u.id.original) - and len(u.id.original.users_collection) > 0 + linked_textobject = u.id[f"{utils.prefix()}_linked_textobject"] + if ( + u.is_updated_geometry + and len(scene.abc3d_data.available_texts) > linked_textobject ): - # must be a new thing, maybe manually created or so - link_text_object_with_new_text_properties(u.id.original, scene) - butils.clean_text_properties() - update_active_text_index() - unlock_depsgraph_updates() + lock_depsgraph_updates() + + def later(): + if butils.lock_depsgraph_update_n_times <= 0: + butils.set_text_on_curve( + scene.abc3d_data.available_texts[linked_textobject] + ) + elif butils.lock_depsgraph_update_n_times <= 0: + butils.lock_depsgraph_update_n_times -= 1 + + butils.run_in_main_thread(later) def register(): @@ -2063,9 +1905,7 @@ def register(): addon_updater_ops.make_annotations(cls) # Avoid blender 2.8 warnings. bpy.utils.register_class(cls) bpy.types.Scene.abc3d_data = bpy.props.PointerProperty(type=ABC3D_data) - bpy.types.Scene.abc3d_font_creation = bpy.props.PointerProperty( - type=ABC3D_PG_FontCreation - ) + bpy.types.Scene.abc3d_font_creation = bpy.props.PointerProperty(type=ABC3D_PG_FontCreation) # bpy.types.Object.__del__ = lambda self: print(f"Bye {self.name}") # autostart if we load a blend file @@ -2080,13 +1920,11 @@ def register(): if on_depsgraph_update not in bpy.app.handlers.depsgraph_update_post: bpy.app.handlers.depsgraph_update_post.append(on_depsgraph_update) - butils.run_in_main_thread(Font.fonts.clear) butils.run_in_main_thread(butils.clear_available_fonts) butils.run_in_main_thread(butils.register_installed_fonts) butils.run_in_main_thread(butils.update_available_fonts) butils.run_in_main_thread(load_used_glyphs) butils.run_in_main_thread(butils.update_types) - butils.run_in_main_thread(detect_text) Font.init() diff --git a/bimport.py b/bimport.py index 85cadbe..27cd8b5 100644 --- a/bimport.py +++ b/bimport.py @@ -4,10 +4,11 @@ from bpy.props import ( BoolProperty, EnumProperty, IntProperty, + FloatProperty, CollectionProperty, ) from bpy.types import Operator -from bpy_extras.io_utils import ImportHelper +from bpy_extras.io_utils import ImportHelper, ExportHelper from io_scene_gltf2 import ConvertGLTF2_Base import importlib @@ -15,31 +16,20 @@ import importlib if "Font" in locals(): importlib.reload(Font) else: - pass + from .common import Font if "utils" in locals(): importlib.reload(utils) else: from .common import utils -try: - from io_scene_gltf2.io.imp.gltf2_io_gltf import glTFImporter, ImportError - from io_scene_gltf2.blender.imp.gltf2_blender_gltf import BlenderGlTF - from io_scene_gltf2.blender.imp.gltf2_blender_vnode import VNode, compute_vnodes - from io_scene_gltf2.blender.com.gltf2_blender_extras import set_extras - from io_scene_gltf2.blender.imp.gltf2_blender_node import BlenderNode -except (ModuleNotFoundError, ImportError): - from io_scene_gltf2.io.imp.gltf2_io_gltf import glTFImporter, ImportError - from io_scene_gltf2.blender.imp.blender_gltf import BlenderGlTF - from io_scene_gltf2.blender.imp.vnode import VNode, compute_vnodes - from io_scene_gltf2.blender.com.extras import set_extras - from io_scene_gltf2.blender.imp.node import BlenderNode - # taken from blender_git/blender/scripts/addons/io_scene_gltf2/__init__.py def get_font_faces_in_file(filepath): + from io_scene_gltf2.io.imp.gltf2_io_gltf import glTFImporter, ImportError + try: import_settings = {"import_user_extensions": []} gltf_importer = glTFImporter(filepath, import_settings) @@ -60,7 +50,7 @@ def get_font_faces_in_file(filepath): out.append(node.extras) return out - except ImportError: + except ImportError as e: return None @@ -70,7 +60,7 @@ def get_font_faces_in_file(filepath): class GetFontFacesInFile(Operator, ImportHelper): """Load a glTF 2.0 font and check which faces are in there""" - bl_idname = "abc3d.check_font_gltf" + bl_idname = f"abc3d.check_font_gltf" bl_label = "Check glTF 2.0 Font" bl_options = {"REGISTER", "UNDO"} @@ -87,6 +77,7 @@ class GetFontFacesInFile(Operator, ImportHelper): def check_gltf2(self, context): import os + import sys if self.files: # Multiple file check @@ -109,7 +100,7 @@ class GetFontFacesInFile(Operator, ImportHelper): class ImportGLTF2(Operator, ConvertGLTF2_Base, ImportHelper): """Load a glTF 2.0 font""" - bl_idname = "abc3d.import_font_gltf" + bl_idname = f"abc3d.import_font_gltf" bl_label = "Import glTF 2.0 Font" bl_options = {"REGISTER", "UNDO"} @@ -294,6 +285,11 @@ class ImportGLTF2(Operator, ConvertGLTF2_Base, ImportHelper): def unit_import(self, filename, import_settings): import time + from io_scene_gltf2.io.imp.gltf2_io_gltf import glTFImporter, ImportError + from io_scene_gltf2.blender.imp.gltf2_blender_gltf import BlenderGlTF + from io_scene_gltf2.blender.imp.gltf2_blender_vnode import VNode, compute_vnodes + from io_scene_gltf2.blender.com.gltf2_blender_extras import set_extras + from io_scene_gltf2.blender.imp.gltf2_blender_node import BlenderNode try: gltf = glTFImporter(filename, import_settings) diff --git a/butils.py b/butils.py index 30b0a47..9b696d1 100644 --- a/butils.py +++ b/butils.py @@ -4,7 +4,6 @@ import queue import re import bpy -import bpy_types import mathutils # import time # for debugging performance @@ -46,17 +45,12 @@ def apply_all_transforms(obj): 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}" +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 @@ -97,8 +91,6 @@ def calc_point_on_bezier(bezier_point_1, bezier_point_2, t): 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 @@ -131,8 +123,6 @@ def calc_tangent_on_bezier(bezier_point_1, bezier_point_2, t): 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 @@ -141,23 +131,6 @@ def calc_tangent_on_bezier(bezier_point_1, bezier_point_2, t): ).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( @@ -217,133 +190,40 @@ def calc_bezier_length(bezier_point_1, bezier_point_2, resolution=20): return length -def get_hook_modifiers(blender_object: bpy.types.Object): - return [m for m in blender_object.modifiers if m.type == "HOOK"] +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 -class HookBezierSplinePoint: - def __init__( - self, - handle_left: mathutils.Vector, - co: mathutils.Vector, - handle_right: mathutils.Vector, - ): - self.handle_left: mathutils.Vector = mathutils.Vector(handle_left) - self.co: mathutils.Vector = mathutils.Vector(co) - self.handle_right: mathutils.Vector = mathutils.Vector(handle_right) - - -class HookBezierSpline: - def __init__( - self, - n: int, - use_cyclic_u: bool, - resolution_u: int, - ): - self.bezier_points = [HookBezierSplinePoint] * n - self.use_cyclic_u: int = use_cyclic_u - self.resolution_u: int = resolution_u - self.beziers: [] - self.lengths: [float] - self.total_length: float - - def calc_length(self, resolution) -> float: - # ignore resolution when accessing length to imitate blender function - return self.total_length - - -class HookBezierData: - def __init__(self, n): - self.splines = [HookBezierSpline] * n - - -class HookBezierCurve: - def __init__(self, blender_curve: bpy.types.Object, resolution_factor=1.0): - self.data = HookBezierData(len(blender_curve.data.splines)) - i = 0 - hooks = get_hook_modifiers(blender_curve) - for si, blender_spline in enumerate(blender_curve.data.splines): - self.data.splines[si] = HookBezierSpline( - len(blender_spline.bezier_points), - blender_spline.use_cyclic_u, - blender_spline.resolution_u, - ) - for pi, blender_bezier_point in enumerate(blender_spline.bezier_points): - self.data.splines[si].bezier_points[pi] = HookBezierSplinePoint( - blender_bezier_point.handle_left, - blender_bezier_point.co, - blender_bezier_point.handle_right, - ) - for hook in hooks: - hook_co = False - hook_handle_left = False - hook_handle_right = False - for vi in hook.vertex_indices: - if vi == i * 3: - hook_handle_left = True - elif vi == i * 3 + 1: - hook_co = True - elif vi == i * 3 + 2: - hook_handle_right = True - if hook_co: - location = ( - blender_curve.matrix_world.inverted() - @ hook.object.matrix_world.translation - ) - self.data.splines[si].bezier_points[pi].co = ( - self.data.splines[si] - .bezier_points[pi] - .co.lerp(location, hook.strength) - ) - if hook_handle_left: - location_left = location + ( - self.data.splines[si].bezier_points[pi].co - - self.data.splines[si].bezier_points[pi].handle_left - ) - self.data.splines[si].bezier_points[pi].handle_left = ( - self.data.splines[si] - .bezier_points[pi] - .co.lerp(location_left, hook.strength) - ) - if hook_handle_right: - location_right = location + ( - self.data.splines[si].bezier_points[pi].co - - self.data.splines[si].bezier_points[pi].handle_right - ) - self.data.splines[si].bezier_points[pi].handle_right = ( - self.data.splines[si] - .bezier_points[pi] - .co.lerp(location, hook.strength) - ) - elif hook_handle_left: - location = ( - blender_curve.matrix_world.inverted() - @ hook.object.matrix_world.translation - ) - self.data.splines[si].bezier_points[pi].handle_left = ( - self.data.splines[si] - .bezier_points[pi] - .handle_left.lerp(location, hook.strength) - ) - elif hook_handle_right: - location = ( - blender_curve.matrix_world.inverted() - @ hook.object.matrix_world.translation - ) - self.data.splines[si].bezier_points[pi].handle_right = ( - self.data.splines[si] - .bezier_points[pi] - .handle_right.lerp(location, hook.strength) - ) - i += 1 - ( - self.data.splines[si].beziers, - self.data.splines[si].lengths, - self.data.splines[si].total_length, - ) = get_real_beziers_and_lengths(self.data.splines[si], resolution_factor) - - -def get_real_beziers_and_lengths(bezier_spline_obj, resolution_factor): beziers = [] lengths = [] total_length = 0 @@ -368,70 +248,6 @@ def get_real_beziers_and_lengths(bezier_spline_obj, resolution_factor): 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) - if not isinstance(bezier_spline_obj, HookBezierSpline) - else ( - bezier_spline_obj.beziers, - bezier_spline_obj.lengths, - bezier_spline_obj.total_length, - ) - ) - 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) - if not isinstance(bezier_spline_obj, HookBezierSpline) - else ( - bezier_spline_obj.beziers, - bezier_spline_obj.lengths, - bezier_spline_obj.total_length, - ) - ) iterated_distance = 0 for i in range(0, len(beziers)): @@ -449,16 +265,7 @@ def calc_point_on_bezier_spline( # 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) @@ -474,9 +281,7 @@ def calc_point_on_bezier_curve( output_spline_index=False, resolution_factor=1.0, ): - bezier_curve = HookBezierCurve(bezier_curve_obj) - curve = bezier_curve.data - # curve = bezier_curve_obj.data + curve = bezier_curve_obj.data # Loop through all splines in the curve total_length = 0 @@ -512,7 +317,6 @@ def calc_point_on_bezier_curve( # 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)] @@ -542,20 +346,19 @@ def find_objects_by_custom_property(objects, 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 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: + if font["font_name"] == font_name and font.parent == None: return font return None @@ -572,7 +375,7 @@ 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: + 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) @@ -583,7 +386,7 @@ def move_in_fontcollection(obj, fontcollection, allow_duplicates=False): # 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: + 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 @@ -604,9 +407,7 @@ def move_in_fontcollection(obj, fontcollection, allow_duplicates=False): 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" - ) + print("found more glyphs objects than expected") # now it must exist glyphs_obj = find_objects_by_name(face_obj.children, startswith="glyphs")[0] @@ -711,15 +512,7 @@ def load_font_from_filepath(filepath, glyphs="", font_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 + face = Font.fonts[mff["font_name"]].faces[mff["face_name"]] # iterate glyphs for g in face.glyphs: # iterate alternates @@ -744,178 +537,27 @@ def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""): # 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 because 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}" - ) + for font_name in Font.fonts.keys(): + for face_name in Font.fonts[font_name].faces.keys(): + found = False + 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"{__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"] +# if "linked_textobject" in o.keys(): +# i = o["linked_textobject"] # found = False # if len(abc3d_data.available_texts) > i: # if abc3d_data.available_texts[i].glyphs @@ -1026,8 +668,6 @@ def completely_delete_objects(objs, recursive=True): except ReferenceError: # not important pass - except RuntimeError: - pass def is_mesh(o): @@ -1038,8 +678,8 @@ 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 + re.match(".*_metrics$", o.name) != None + or re.match(".*_metrics.[\d]{3}$", o.name) != None ) and is_mesh(o) @@ -1052,12 +692,12 @@ def is_text_object(o): return False -def is_glyph_object(o): +def is_glyph(o): if f"{utils.prefix()}_type" in o: return o[f"{utils.prefix()}_type"] == "glyph" try: return ( - o.parent is not None + type(o.parent) is not type(None) and "glyphs" in o.parent.name and is_mesh(o) and not is_metrics_object(o) @@ -1066,10 +706,6 @@ def is_glyph_object(o): return False -def is_glyph(o): - return is_glyph_object(o) - - def update_types(): scene = bpy.context.scene abc3d_data = scene.abc3d_data @@ -1096,7 +732,6 @@ def get_glyph_advance(glyph_obj): 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): @@ -1112,53 +747,25 @@ def get_glyph_height(glyph_obj): 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 + loaded, missing, loadable, files = Font.test_glyphs_availability( + font_name, face_name, text + ) # possibly replace upper and lower case letters with each other - if len(availability.missing) > 0 and allow_replacement: + if len(missing) > 0 and allow_replacement: replacement_search = "" - for m in availability.missing: + for m in missing: if m.isalpha(): replacement_search += m.swapcase() r = Font.test_availability(font_name, face_name, replacement_search) - loadable += r.unloaded + loadable += r["maybe"] # not update (loaded, missing, files), we only use loadable/maybe later if len(loadable) > 0: - for filepath in availability.filepaths: + for filepath in files: 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: - C = c.swapcase() - if C in AVAILABILITY.missing: - t_text = t_text.replace(c, "") - else: - t_text = t_text.replace(c, C) - return t_text - - def is_bezier(curve): if curve.type != "CURVE": return False @@ -1170,416 +777,10 @@ def is_bezier(curve): 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 get_text_properties_by_index(text_index, scene=None): - if scene is None: - scene = bpy.context.scene - abc3d_data = scene.abc3d_data - if text_index >= len(abc3d_data.available_texts): - return None - return abc3d_data.available_texts[text_index] - - -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 - 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 - glyph_properties["glyph_index"] = glyph_index - glyph_properties["text_id"] = text_properties.text_id - glyph_object["text_id"] = text_properties.text_id - 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 get_text_difference_index(text_a, text_b): - len_a = len(text_a) - len_b = len(text_b) - len_min = min(len_a, len_b) - len_max = max(len_a, len_b) - for i in range(0, len_max): - if i >= len_min or text_a[i] != text_b[i]: - return i - return False - - def would_regenerate(text_properties): - predicted_text = predict_actual_text(text_properties) - if text_properties.actual_text != predicted_text: - return get_text_difference_index(text_properties.actual_text, predicted_text) - if len(text_properties.glyphs) == 0: + mom = text_properties.text_object + + if len(text_properties.text) != len(text_properties.glyphs): return True for i, g in enumerate(text_properties.glyphs): @@ -1588,9 +789,9 @@ def would_regenerate(text_properties): elif g.glyph_object.type != "EMPTY": return True # check if perhaps one glyph was deleted - elif g.glyph_object is None: + elif type(g.glyph_object) == type(None): return True - elif g.glyph_object.parent is None: + elif type(g.glyph_object.parent) == type(None): return True elif g.glyph_object.parent.users_collection != g.glyph_object.users_collection: return True @@ -1610,11 +811,10 @@ 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 - ) - + 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: @@ -1628,208 +828,22 @@ def is_or_has_parent(o, parent, if_is_parent=True, max_depth=10): return False return False - def parent_to_curve(o, c): - # https://projects.blender.org/blender/blender/issues/100661 - o.parent_type = "OBJECT" + 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": + 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 get_original_glyph(text_properties, glyph_properties): - glyph_tmp = Font.get_glyph( - text_properties.font_name, - text_properties.face_name, - glyph_properties.glyph_id, - glyph_properties.alternate, - ) - if glyph_tmp is None: - return None - return glyph_tmp.original - - -def ensure_glyph_object(text_properties, glyph_properties): - glyph_index = glyph_properties["glyph_index"] - # First, let's see if there was ever a glyph object constructed - if ( - glyph_properties.glyph_object is None - or not isinstance(glyph_properties.glyph_object, bpy_types.Object) - or not is_glyph_object(glyph_properties.glyph_object) - ): - # we do need a text_object though - # if there is not, let's give up for this iteration - if not isinstance(text_properties.text_object, bpy_types.Object): - print( - f"{utils.prefix()}::ensure_glyph_object: failed! text object is not an object" - ) - return False - - outer_node = bpy.data.objects.new(f"{glyph_properties.glyph_id}", None) - inner_node = bpy.data.objects.new( - f"{glyph_properties.glyph_id}_mesh", - get_original_glyph(text_properties, glyph_properties).data, - ) - transfer_properties_to_glyph_object( - text_properties, glyph_properties, outer_node - ) - - # Add into the scene. - text_properties.text_object.users_collection[0].objects.link(outer_node) - text_properties.text_object.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, text_properties.text_object) - - # outer_node["inner_node"] = bpy.types.PointerProperty(inner_node) - # for some funny reason we cannot set 'glyph_object' by key, but need to set the attribute - 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 - - # we might just want to update the data - # imagine a different font, letter or alternate - # this way we keep all manual transforms - if ( - glyph_properties.glyph_object[get_key("glyph_id")] != glyph_properties.glyph_id - or glyph_properties.glyph_object[get_key("alternate")] - != glyph_properties.alternate - or glyph_properties.glyph_object[get_key("font_name")] - != text_properties.font_name - or glyph_properties.glyph_object[get_key("face_name")] - != text_properties.face_name - ): - - inner_node = None - old_font_name = glyph_properties.glyph_object[get_key("font_name")] - old_face_name = glyph_properties.glyph_object[get_key("face_name")] - old_face = Font.get_font_face(old_font_name, old_face_name) - face = Font.get_font_face(text_properties.font_name, text_properties.face_name) - ratio = old_face.unit_factor / face.unit_factor - # try: - # inner_node = glyph_properties["inner_node"].original - # inner_node.location = inner_node.location * ratio - # except KeyError: - old_glyph_id = glyph_properties.glyph_object[get_key("glyph_id")] - for c in glyph_properties.glyph_object.children: - if c.name.startswith(f"{old_glyph_id}_mesh"): - inner_node = c - inner_node.location = inner_node.location * ratio - inner_node.name = f"{glyph_properties.glyph_id}_mesh" - # outer_node["inner_node"] = bpy.types.PointerProperty(inner_node) - if inner_node is None: - print(f"{utils.prefix()}::ensure_glyph_object: failed! no inner_node found") - return False - inner_node.data = get_original_glyph(text_properties, glyph_properties).data - glyph_properties.glyph_object[get_key("glyph_id")] = glyph_properties.glyph_id - glyph_properties.glyph_object[get_key("alternate")] = glyph_properties.alternate - glyph_properties.glyph_object[get_key("font_name")] = text_properties.font_name - glyph_properties.glyph_object[get_key("face_name")] = text_properties.face_name - - glyph_properties.glyph_object.hide_set(True) - - return True - - -def ensure_glyphs(text_properties, predicted_text: str): - - ######### REQUIREMENTS - - # turns out this is not a requirement - # and can be a case we want to tackle - # - # if not text_properties.get("glyphs"): - # ShowMessageBox( - # title="text_properties has no glyphs", message="well, what I said" - # ) - # return False - - ######### SETUP - - n_glyphs = len(text_properties.glyphs) - n_predicted = len(predicted_text) - - ########## ENSURE AMOUNT - - if n_glyphs == n_predicted: - # same amount of glyphs - # this is the most common case - # don't do anything - pass - - elif n_glyphs > n_predicted: - # more glyphs than predicted - # it's a shorter word, or letters were deleted - count = n_glyphs - n_predicted - for i in range(0, count): - reverse_i = n_glyphs - (i + 1) - # let's attempt to remove the glyph_object first - # so we avoid dangling data - if isinstance( - text_properties.glyphs[reverse_i].glyph_object, bpy_types.Object - ): - # bam! - completely_delete_objects( - [text_properties.glyphs[reverse_i].glyph_object] - ) - # else: - # # nothing to do, if there is no blender object - # # possibly we could do a 'del', but we can also - # # just comment out the whole conditional fork - # pass - - # now that blender data is gone, we can remove the glyph - text_properties.glyphs.remove(reverse_i) - - elif n_glyphs < n_predicted: - # less glyphs than predicted - # it's a longer word, or letters were added - while n_glyphs < n_predicted: - glyph_id = predicted_text[n_glyphs] - glyph_properties = text_properties.glyphs.add() - glyph_properties["glyph_id"] = predicted_text[n_glyphs] - glyph_properties["glyph_index"] = n_glyphs - glyph_properties["text_id"] = text_properties.text_id - glyph_properties["letter_spacing"] = 0 - n_glyphs += 1 - - ######### ENSURE VALUES - - for i, glyph_properties in enumerate(text_properties.glyphs): - glyph_properties["glyph_index"] = i - glyph_properties["text_id"] = text_properties.text_id - glyph_properties["glyph_id"] = predicted_text[i] - if not ensure_glyph_object(text_properties, glyph_properties): - print(f"{utils.prefix()}::ensure_glyphs: could not ensure glyph_object") - transfer_text_properties_to_text_object( - text_properties, text_properties.text_object - ) - - return True - - -# C.scene.abc3d_data.available_texts[0] -# import abc3d -# abc3d.butils.ensure_glyphs(C.scene.abc3d_data.available_texts[0], "whatever") - - -def set_text_on_curve( - text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4, can_regenerate=False -): +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. @@ -1847,20 +861,42 @@ def set_text_on_curve( # global lock_depsgraph_update_n_times # starttime = time.perf_counter_ns() - if text_properties is None: - return False mom = text_properties.text_object - if mom is None: - return False if mom.type != "CURVE": return False - if len(mom.users_collection) < 1: - return False distribution_type = "CALCULATE" if is_bezier(mom) else "FOLLOW_PATH" - predicted_text = predict_actual_text(text_properties) - ensure_glyphs(text_properties, predicted_text) + # 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"): + for g in text_properties.glyphs: + print(dict(g)) + glyph_objects = [g["glyph_object"] for g in text_properties["glyphs"]] + completely_delete_objects(glyph_objects, True) + text_properties.glyphs.clear() + + mom[f"{utils.prefix()}_type"] = "textobject" + mom[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id + mom[f"{utils.prefix()}_font_name"] = text_properties.font_name + mom[f"{utils.prefix()}_face_name"] = text_properties.face_name + mom[f"{utils.prefix()}_font_size"] = text_properties.font_size + mom[f"{utils.prefix()}_letter_spacing"] = text_properties.letter_spacing + mom[f"{utils.prefix()}_orientation"] = text_properties.orientation + mom[f"{utils.prefix()}_translation"] = text_properties.translation curve_length = get_curve_length(mom) advance = text_properties.offset @@ -1869,12 +905,8 @@ def set_text_on_curve( is_command = False previous_spline_index = -1 - actual_text = "" - # we need to iterate over the original text, as we want commands - # however, ideally it could be an array of glyphs, commands and spaces - # now we need to handle non existing characters etc everytime in the loop for i, c in enumerate(text_properties.text): - face = Font.get_font_face(text_properties.font_name, text_properties.face_name) + face = Font.fonts[text_properties.font_name].faces[text_properties.face_name] scalor = face.unit_factor * text_properties.font_size if c == "\\": is_command = True @@ -1896,31 +928,88 @@ def set_text_on_curve( spline_index = 0 - ############### HANDLE SPACES + ############### GET GLYPH - if glyph_id not in predicted_text: + 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 + 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] - # ensure_glyph_object(text_properties, glyph_properties) + glyph_properties = text_properties.glyphs[glyph_index] if not regenerate else text_properties.glyphs.add() - ############### ACTUAL TEXT - - actual_text += glyph_id + if regenerate: + glyph_properties["glyph_id"] = glyph_id + glyph_properties["text_id"] = text_properties.text_id + glyph_properties["letter_spacing"] = 0 ############### NODE SCENE MANAGEMENT - # outsourced to ensure_glyph_object + 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) + outer_node[f"{utils.prefix()}_type"] = "glyph" + outer_node[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id + outer_node[f"{utils.prefix()}_glyph_index"] = glyph_index + outer_node[f"{utils.prefix()}_font_name"] = text_properties.font_name + outer_node[f"{utils.prefix()}_face_name"] = text_properties.face_name + + # Add into the scene. + mom.users_collection[0].objects.link(outer_node) + mom.users_collection[0].objects.link(inner_node) + # bpy.context.scene.collection.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 + 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 - glyph = get_original_glyph(text_properties, glyph_properties) - # origins could be shifted # so we need to apply a pre_advance glyph_pre_advance, glyph_post_advance = get_glyph_prepost_advances(glyph) @@ -1940,37 +1029,30 @@ def set_text_on_curve( 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"].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_glyph_object_rotation_mode = None - if glyph_properties.glyph_object.rotation_mode != "QUATERNION": - previous_glyph_object_rotation_mode = ( - glyph_properties.glyph_object.rotation_mode - ) - glyph_properties.glyph_object.rotation_mode = "QUATERNION" + 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 - ) - # location, tangent, spline_index = calc_point_on_bezier_curve( - # mom_hooked, applied_advance, True, True - # ) + 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 - glyph_properties.glyph_object.location = ( - location + text_properties.translation - ) + outer_node.location = location + text_properties.translation # orientation / rotation mask = [0] @@ -1978,47 +1060,41 @@ def set_text_on_curve( 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()] - ) + 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) - glyph_properties.glyph_object.rotation_quaternion = ( - motor[0].to_3x3() @ q.to_matrix() - ).to_quaternion() + 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: - # glyph_properties.glyph_object.rotation_quaternion = (mom.matrix_world.inverted().to_3x3() @ motor[0].to_3x3() @ q.to_matrix()).to_quaternion() + # outer_node.rotation_quaternion = (mom.matrix_world.inverted().to_3x3() @ motor[0].to_3x3() @ q.to_matrix()).to_quaternion() # # scale - glyph_properties.glyph_object.scale = (scalor, scalor, scalor) + outer_node.scale = (scalor, scalor, scalor) - if previous_glyph_object_rotation_mode: - glyph_properties.glyph_object.rotation_mode = ( - previous_glyph_object_rotation_mode - ) + 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 + 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 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 @@ -2027,60 +1103,77 @@ def set_text_on_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 - ): + previous_location - new_location + ).length > glyph_advance and psi == si: curve_compensation = curve_compensation - glyph_advance * 0.01 - tmp_new_location, si = calc_point_on_bezier_curve( + 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 - ): + previous_location - new_location + ).length < glyph_advance and psi == si: curve_compensation = curve_compensation + glyph_advance * 0.01 - tmp_new_location, si = calc_point_on_bezier_curve( + 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 - text_properties["actual_text"] = actual_text + # NOTE: depsgraph update not locked + # as we fixed data_path with parent_to_curve trick + # if lock_depsgraph_update_n_times < 0: + # lock_depsgraph_update_n_times = len( + # bpy.context.selected_objects + # ) + # else: + # lock_depsgraph_update_n_times += len( + # bpy.context.selected_objects + # ) + # # NOTE: we reset with a timeout, as setting and resetting certain things + # # in fast succession will cause visual glitches (e.g. {}.data.use_path). + # def reset(): + # mom.data.use_path = previous_use_path + # if counted_reset in bpy.app.handlers.depsgraph_update_post: + # bpy.app.handlers.depsgraph_update_post.remove(counted_reset) + # if bpy.app.timers.is_registered(reset): + # bpy.app.timers.unregister(reset) + # molotov = reset_depsgraph_n + 0 + # def counted_reset(scene, depsgraph): + # nonlocal molotov + # if molotov == 0: + # reset() + # else: + # molotov -= 1 + # # unregister previous resets to avoid multiple execution + # if bpy.app.timers.is_registered(reset): + # bpy.app.timers.unregister(reset) + # if counted_reset in bpy.app.handlers.depsgraph_update_post: + # bpy.app.handlers.depsgraph_update_post.remove(counted_reset) + # if not isinstance(reset_timeout_s, bool): + # if reset_timeout_s > 0: + # bpy.app.timers.register(reset, first_interval=reset_timeout_s) + # elif reset_timeout <= 0: + # reset() + # bpy.app.handlers.depsgraph_update_post.append(counted_reset) + + # endtime = time.perf_counter_ns() + # elapsedtime = endtime - starttime return True verification_object = { f"{utils.prefix()}_type": "textobject", - f"{utils.prefix()}_text_id": 0, + f"{utils.prefix()}_linked_textobject": 0, f"{utils.prefix()}_font_name": "font_name", f"{utils.prefix()}_face_name": "face_name", f"{utils.prefix()}_font_size": 42, @@ -2094,6 +1187,28 @@ def verify_text_object(o): pass +def transfer_text_properties_to_text_object(text_properties, o): + o[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id + o[f"{utils.prefix()}_font_name"] = text_properties.font_name + o[f"{utils.prefix()}_face_name"] = text_properties.face_name + o[f"{utils.prefix()}_font_size"] = text_properties.font_size + o[f"{utils.prefix()}_letter_spacing"] = text_properties.letter_spacing + o[f"{utils.prefix()}_orientation"] = text_properties.orientation + o[f"{utils.prefix()}_translation"] = text_properties.translation + o[f"{utils.prefix()}_text"] = text_properties["text"] + + +def transfer_text_object_to_text_properties(o, text_properties): + text_properties["text_id"] = o[f"{utils.prefix()}_linked_textobject"] + text_properties["font_name"] = o[f"{utils.prefix()}_font_name"] + text_properties["face_name"] = o[f"{utils.prefix()}_face_name"] + text_properties["font_size"] = o[f"{utils.prefix()}_font_size"] + text_properties["letter_spacing"] = o[f"{utils.prefix()}_letter_spacing"] + text_properties["orientation"] = o[f"{utils.prefix()}_orientation"] + text_properties["translation"] = o[f"{utils.prefix()}_translation"] + text_properties["text"] = o[f"{utils.prefix()}_text"] + + # blender bound_box vertices # # 3------7. @@ -2466,7 +1581,6 @@ def align_metrics_of_objects(objects=None): 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 @@ -2489,87 +1603,86 @@ def align_origins_to_active_object(objects=None, axis=2): 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)]) + # 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 + # 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 + # 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" + # 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 "" + # 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" + # 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] + # 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 + # metrics_origin_x = c.matrix_world.translation[0] + min_x + + # diff = metrics_origin_x - o.matrix_world.translation[0] -# diff = metrics_origin_x - o.matrix_world.translation[0] + # for v in o.data.vertices: + # v.co[0] -= diff -# for v in o.data.vertices: -# v.co[0] -= diff + # o.location += mathutils.Vector((diff, 0.0, 0.0)) @ o.matrix_world.inverted() -# 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() -# for c in o.children: -# if is_metrics_object(c): -# c.location -= mathutils.Vector((diff, 0.0, 0.0)) @ o.matrix_world.inverted() - -# return "" + # return "" diff --git a/common/Font.py b/common/Font.py index aebf702..bcb7949 100644 --- a/common/Font.py +++ b/common/Font.py @@ -1,5 +1,5 @@ +from typing import Dict from pathlib import Path -from typing import Dict, NamedTuple # convenience dictionary for translating names to glyph ids # note: overwritten/extended by the content of "glypNamesToUnicode.txt" @@ -163,7 +163,7 @@ class Font: def register_font(font_name, face_name, glyphs_in_fontfile, filepath): if not fonts.keys().__contains__(font_name): fonts[font_name] = Font({}) - if fonts[font_name].faces.get(face_name) is None: + if fonts[font_name].faces.get(face_name) == None: fonts[font_name].faces[face_name] = FontFace({}) fonts[font_name].faces[face_name].glyphs_in_fontfile = glyphs_in_fontfile else: @@ -177,34 +177,6 @@ def register_font(font_name, face_name, glyphs_in_fontfile, filepath): fonts[font_name].faces[face_name].filepaths.append(filepath) -def get_font(font_name): - if not fonts.keys().__contains__(font_name): - print(f"ABC3D::get_font: font name({font_name}) not found") - print(fonts.keys()) - return None - return fonts[font_name] - - -def get_font_face(font_name, face_name): - font = get_font(font_name) - if font is None: - return None - if not font.faces.keys().__contains__(face_name): - print( - f"ABC3D::get_font_face (font: {font_name}): face name({face_name}) not found" - ) - print(font.faces.keys()) - return None - return font.faces[face_name] - - -def get_font_face_filepaths(font_name, face_name): - face = get_font_face(font_name, face_name) - if not face: - return None - return face.filepaths - - def add_glyph(font_name, face_name, glyph_id, glyph_object): """add_glyph adds a glyph to a FontFace it creates the :class:`Font` and :class:`FontFace` if it does not exist yet @@ -221,9 +193,9 @@ def add_glyph(font_name, face_name, glyph_id, glyph_object): if not fonts.keys().__contains__(font_name): fonts[font_name] = Font({}) - if fonts[font_name].faces.get(face_name) is None: + if fonts[font_name].faces.get(face_name) == None: fonts[font_name].faces[face_name] = FontFace({}) - if fonts[font_name].faces[face_name].glyphs.get(glyph_id) is None: + if fonts[font_name].faces[face_name].glyphs.get(glyph_id) == None: fonts[font_name].faces[face_name].glyphs[glyph_id] = [] fonts[font_name].faces[face_name].glyphs.get(glyph_id).append(glyph_object) @@ -231,48 +203,7 @@ def add_glyph(font_name, face_name, glyph_id, glyph_object): fonts[font_name].faces[face_name].loaded_glyphs.append(glyph_id) -def get_glyphs(font_name, face_name, glyph_id): - """get_glyphs returns an array of glyphs of a FontFace - - :param font_name: The :class:`Font` you want to get the glyph from - :type font_name: str - :param face_name: The :class:`FontFace` you want to get the glyph from - :type face_name: str - :param glyph_id: The ``glyph_id`` from the glyph you want - :type glyph_id: str - ... - :return: returns a list of the glyph objects, or an empty list if none exists - :rtype: `List` - """ - - face = get_font_face(font_name, face_name) - if face is None: - print(f"ABC3D::get_glyph: font({font_name}) face({face_name}) not found") - try: - print(fonts[font_name].faces.keys()) - except: - print(fonts.keys()) - return [] - - glyphs_for_id = face.glyphs.get(glyph_id) - if glyphs_for_id is None: - print( - f"ABC3D::get_glyph: font({font_name}) face({face_name}) glyph({glyph_id}) not found" - ) - if glyph_id not in fonts[font_name].faces[face_name].missing_glyphs: - fonts[font_name].faces[face_name].missing_glyphs.append(glyph_id) - return [] - - return glyphs_for_id - - -def get_glyph( - font_name: str, - face_name: str, - glyph_id: str, - alternate: int = 0, - alternate_tolerant: bool = True, -): +def get_glyph(font_name, face_name, glyph_id, alternate=0): """add_glyph adds a glyph to a FontFace it creates the :class:`Font` and :class:`FontFace` if it does not exist yet @@ -282,79 +213,56 @@ def get_glyph( :type face_name: str :param glyph_id: The ``glyph_id`` from the glyph you want :type glyph_id: str - :param alternate: The ``alternate`` from the glyph you want - :type alternate: int - :param alternate_tolerant: Fetch an existing alternate if requested is out of bounds - :type glyph_id: bool ... :return: returns the glyph object, or ``None`` if it does not exist :rtype: `Object` """ - glyphs = get_glyphs(font_name, face_name, glyph_id) - - if len(glyphs) == 0: - print( - f"ABC3D::get_glyph: font({font_name}) face({face_name}) glyph({glyph_id})[{alternate}] not found" - ) + if not fonts.keys().__contains__(font_name): + # print(f"ABC3D::get_glyph: font name({font_name}) not found") + # print(fonts.keys()) return None - if len(glyphs) <= alternate: - if alternate_tolerant: - alternate = 0 - else: - print( - f"ABC3D::get_glyph: font({font_name}) face({face_name}) glyph({glyph_id})[{alternate}] not found" - ) - return None + face = fonts[font_name].faces.get(face_name) + if face == None: + # print(f"ABC3D::get_glyph: font({font_name}) face({face_name}) not found") + # print(fonts[font_name].faces.keys()) + return None - return glyphs[alternate] + glyphs_for_id = face.glyphs.get(glyph_id) + if glyphs_for_id == None or len(glyphs_for_id) <= alternate: + # print(f"ABC3D::get_glyph: font({font_name}) face({face_name}) glyph({glyph_id})[{alternate}] not found") + if glyph_id not in fonts[font_name].faces[face_name].missing_glyphs: + fonts[font_name].faces[face_name].missing_glyphs.append(glyph_id) + return None - -def unloaded_glyph(font_name, face_name, glyph_id): - face = get_font_face(font_name, face_name) - if face is None: - print(f"ABC3D::get_glyph: font({font_name}) face({face_name}) not found") - return - while True: - try: - fonts[font_name].faces[face_name].loaded_glyphs.remove(glyph_id) - del fonts[font_name].faces[face_name].glyphs[glyph_id] - except ValueError: - break - - -class GlyphsAvailability(NamedTuple): - loaded: str - missing: str - unloaded: str - filepaths: list[str] + return fonts[font_name].faces[face_name].glyphs.get(glyph_id)[alternate] def test_glyphs_availability(font_name, face_name, text): # maybe there is NOTHING yet if ( not fonts.keys().__contains__(font_name) - or fonts[font_name].faces.get(face_name) is None + or fonts[font_name].faces.get(face_name) == None ): - return GlyphsAvailability("", "", "", []) + return "", "", text # , , loaded = [] missing = [] - unloaded = [] + maybe = [] for c in text: if c in fonts[font_name].faces[face_name].loaded_glyphs: loaded.append(c) elif c in fonts[font_name].faces[face_name].glyphs_in_fontfile: - unloaded.append(c) + maybe.append(c) else: if c not in fonts[font_name].faces[face_name].missing_glyphs: fonts[font_name].faces[face_name].missing_glyphs.append(c) missing.append(c) - return GlyphsAvailability( + return ( "".join(loaded), "".join(missing), - "".join(unloaded), + "".join(maybe), fonts[font_name].faces[face_name].filepaths, ) @@ -378,12 +286,17 @@ MISSING_FACE = 1 def test_availability(font_name, face_name, text): if not fonts.keys().__contains__(font_name): return MISSING_FONT - if fonts[font_name].faces.get(face_name) is None: + if fonts[font_name].faces.get(face_name) == None: return MISSING_FACE - availability: GlyphsAvailability = test_glyphs_availability( + loaded, missing, maybe, filepaths = test_glyphs_availability( font_name, face_name, text ) - return availability + return { + "loaded": loaded, + "missing": missing, + "maybe": maybe, + "filepaths": filepaths, + } # holds all fonts diff --git a/common/spacesUnicode.txt b/common/spacesUnicode.txt index b7270e0..da6a7c9 100644 --- a/common/spacesUnicode.txt +++ b/common/spacesUnicode.txt @@ -4,20 +4,20 @@ space 0020 0.25 nbspace 00A0 0.25 # ethi:wordspace 1361 # NOTE: has shape enquad 2000 0.5 -emquad 2001 1.0 +emquad 2001 1 enspace 2002 0.5 -emspace 2003 1.0 -threeperemspace 2004 3.0 -fourperemspace 2005 4.0 -sixperemspace 2006 6.0 -figurespace 2007 1.0 -punctuationspace 2008 1.0 +emspace 2003 1 +threeperemspace 2004 3 +fourperemspace 2005 4 +sixperemspace 2006 6 +figurespace 2007 1 +punctuationspace 2008 1 thinspace 2009 0.1 hairspace 200A 0.05 -zerowidthspace 200B 0.0 +zerowidthspace 200B 0 narrownobreakspace 202F 0.1 -mediummathematicalspace 205F 1.0 +mediummathematicalspace 205F 1 cntr:space 2420 0.25 -ideographicspace 3000 1.0 +ideographicspace 3000 1 # ideographichalffillspace 303F # NOTE: has shape -zerowidthnobreakspace FEFF 0.0 +zerowidthnobreakspace FEFF 0 diff --git a/common/utils.py b/common/utils.py index 7798a3c..8219a94 100644 --- a/common/utils.py +++ b/common/utils.py @@ -8,11 +8,11 @@ def get_version_minor(): def get_version_patch(): - return 12 + return 7 def get_version_string(): - return f"{get_version_major()}.{get_version_minor()}.{get_version_patch()}" + return f"{get_version_major()}.{get_version_minor()}.{get_version_patch}" def prefix(): @@ -23,6 +23,7 @@ import datetime import time + def get_timestamp(): return datetime.datetime.fromtimestamp(time.time()).strftime("%Y.%m.%d-%H:%M:%S") @@ -81,10 +82,6 @@ def open_file_browser(directory): # xdg-open *should* be supported by recent Gnome, KDE, Xfce -def LINE(): - return sys._getframe(1).f_lineno - - def printerr(*args, **kwargs): print(*args, file=sys.stderr, **kwargs) @@ -93,34 +90,6 @@ def removeNonAlphabetic(s): return "".join([i for i in s if i.isalpha()]) -import os -import pathlib - - -def can_create_path(path_str: str): - path = pathlib.Path(path_str).absolute().resolve() - - tries = 0 - maximum_tries = 1000 - while True: - if path.exists(): - if os.access(path, os.W_OK): - return True - else: - return False - elif path == path.parent: - # should never be reached, because root exists - # but if it doesn't.. well then we can't - return False - - path = path.parent - tries += 1 - if tries > maximum_tries: - # always, always break out of while loops eventually - # IF you don't want to be here forever - break - - # # Evaluate a bezier curve for the parameter 0<=t<=1 along its length # def evaluateBezierPoint(p1, h1, h2, p2, t): # return ((1 - t)**3) * p1 + (3 * t * (1 - t)**2) * h1 + (3 * (t**2) * (1 - t)) * h2 + (t**3) * p2 diff --git a/requirements.txt b/requirements.txt index ada7e47..fcb9ea9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,39 +1,33 @@ -asttokens==3.0.0 -attrs==25.3.0 -bpy==4.4.0 -cattrs==24.1.3 -certifi==2025.4.26 -charset-normalizer==3.4.2 -Cython==3.1.1 -decorator==5.2.1 -docstring-to-markdown==0.17 -executing==2.2.0 +astroid==3.3.5 +attrs==24.2.0 +black==24.10.0 +bpy==4.2.0 +cattrs==24.1.2 +certifi==2024.8.30 +charset-normalizer==3.4.0 +click==8.1.7 +Cython==3.0.11 +dill==0.3.9 +docstring-to-markdown==0.15 +flake8==7.1.1 idna==3.10 -importlib_metadata==8.7.0 -ipython==9.2.0 -ipython_pygments_lexers==1.1.1 -jedi==0.19.2 -jedi-language-server==0.45.1 +isort==5.13.2 +jedi==0.19.1 +jedi-language-server==0.41.4 lsprotocol==2023.0.1 mathutils==3.3.0 -matplotlib-inline==0.1.7 -numpy==1.26.4 +mccabe==0.7.0 +mypy-extensions==1.0.0 +numpy==2.1.3 +packaging==24.1 parso==0.8.4 -pexpect==4.9.0 -pluggy==1.6.0 -prompt_toolkit==3.0.51 -ptyprocess==0.7.0 -pure_eval==0.2.3 +pathspec==0.12.1 +platformdirs==4.3.6 +pycodestyle==2.12.1 +pyflakes==3.2.0 pygls==1.3.1 -Pygments==2.19.1 -python-jsonrpc-server==0.4.0 -python-lsp-jsonrpc==1.1.2 +pylint==3.3.1 requests==2.32.3 -stack-data==0.6.3 -traitlets==5.14.3 -typing_extensions==4.13.2 -ujson==5.10.0 -urllib3==2.4.0 -wcwidth==0.2.13 -zipp==3.22.0 +tomlkit==0.13.2 +urllib3==2.2.3 zstandard==0.23.0 diff --git a/testing_scripts/bezier_distance.py b/testing_scripts/bezier_distance.py deleted file mode 100644 index 5557a6b..0000000 --- a/testing_scripts/bezier_distance.py +++ /dev/null @@ -1,25 +0,0 @@ -import bpy -from mathutils import * -from math import * -import abc3d.butils - -v = 0 -goal = 5.0 -step = 0.1 -speed = 1.0 - -C = bpy.context -obj = C.scene.objects['Cube'] -curve = C.scene.objects['BézierCurve'] - -m = curve.matrix - -def fun(distance): - obj.location = m @ abc3d.butils.calc_point_on_bezier_curve(curve, - distance, - output_tangent=True) - print(f"executed {distance}") - -while v < goal: - bpy.app.timers.register(lambda: fun(v), first_interval=(v * speed)) - v += step diff --git a/testing_scripts/unload_glyphs.py b/testing_scripts/unload_glyphs.py deleted file mode 100644 index 3802a58..0000000 --- a/testing_scripts/unload_glyphs.py +++ /dev/null @@ -1,115 +0,0 @@ -import bpy - -import abc3d -from abc3d import butils -from abc3d.common import Font - - -def get_text_properties_by_mom(mom): - scene = bpy.context.scene - abc3d_data = scene.abc3d_data - - for text_properties in abc3d_data.available_texts: - if mom == text_properties.text_object: - return text_properties - return None - - -def isolate_objects(objects): - for area in bpy.context.window.screen.areas: - if area.type == "VIEW_3D": - with bpy.context.temp_override( - selected_objects=list(objects), - area=area, - refgion=[region for region in area.regions if region.type == "WINDOW"][ - 0 - ], - screen=bpy.context.window.screen, - ): - # bpy.ops.view3d.view_selected() - bpy.ops.view3d.localview(frame_selected=True) - break - - -def main(): - # create a curve - bpy.ops.curve.primitive_bezier_curve_add( - radius=1, - enter_editmode=False, - align="WORLD", - location=(0, 0, 0), - scale=(1, 1, 1), - ) - # new curve is active object - mom = bpy.context.active_object - - # make sure - print(f"MOM: {mom.name}") - - fonts = Font.get_loaded_fonts_and_faces() - if len(fonts) == 0: - print("no fonts! what?") - return - - font_name = fonts[0][0] - face_name = fonts[0][1] - font = f"{font_name} {face_name}" - - isolate_objects([mom]) - - bpy.ops.abc3d.placetext( - font_name=font_name, - face_name=face_name, - font=font, - text="SOMETHING SOMETHING BROKEN ARMS", - letter_spacing=0, - font_size=1, - offset=0, - translation=(0, 0, 0), - orientation=(1.5708, 0, 0), - ) - - def change_text(font_name="", face_name="", text=""): - print(f"change_text to '{text}'") - text_properties = get_text_properties_by_mom(mom) - if font_name != "": - text_properties["font_name"] = font_name - if face_name != "": - text_properties["face_name"] = face_name - if text != "": - text_properties.text = text - else: - text_properties.text = text_properties.text - return None - - def unload(glyph_id): - print(f"unload glyph '{glyph_id}'") - butils.unload_unused_glyph(font_name, face_name, glyph_id) - return None - - def unload_all(): - print(f"unload glyph all unused glyphs") - butils.unload_unused_glyphs() - return None - - bpy.app.timers.register(lambda: change_text(text="SOMETHING"), first_interval=0) - bpy.app.timers.register(lambda: change_text(text="LOLSS"), first_interval=2) - bpy.app.timers.register(lambda: change_text(text="LOLAA"), first_interval=3) - bpy.app.timers.register(lambda: change_text(text="WHAT"), first_interval=4) - bpy.app.timers.register(lambda: change_text(text="LOL"), first_interval=5) - - bpy.app.timers.register(lambda: unload("A"), first_interval=10) - bpy.app.timers.register(lambda: unload_all(), first_interval=12) - - bpy.app.timers.register(lambda: change_text(text="LOLM"), first_interval=16) - bpy.app.timers.register(lambda: change_text(text="ZHE END"), first_interval=20) - - bpy.app.timers.register( - lambda: change_text(font_name="NM_Origin", face_name="Tender"), - first_interval=30, - ) - - bpy.app.timers.register(lambda: unload_all(), first_interval=42) - - -main()