diff --git a/README.md b/README.md index 0b0bdee..e38ad46 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ / ___ \| |_) | |___ ___) | |_| | /_/ \_\____/ \____|____/|____/ ``` -v0.0.5 +v0.0.6 Convenience tool to work with 3D typography in Blender and Cinema4D. diff --git a/__init__.py b/__init__.py index 9d5b8be..2a71c8f 100644 --- a/__init__.py +++ b/__init__.py @@ -16,13 +16,13 @@ from .common import Font, utils bl_info = { "name": "ABC3D", "author": "Jakob Schlötter, Studio Pointer*", - "version": (0, 0, 5), + "version": (0, 0, 6), "blender": (4, 1, 0), "location": "VIEW3D", "description": "Convenience addon for 3D fonts", "category": "Typography", } -# NOTE: also change version in common/utils.py +# NOTE: also change version in common/utils.py and README.md # make sure that modules are reloadable # when registering @@ -134,11 +134,26 @@ class ABC3D_available_font(bpy.types.PropertyGroup): 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] + ) + glyph_id: bpy.props.StringProperty(maxlen=1) + text_id: bpy.props.IntProperty( + default=-1, + ) + alternate: bpy.props.IntProperty( + default=-1, + update=update_callback, + ) glyph_object: bpy.props.PointerProperty(type=bpy.types.Object) letter_spacing: bpy.props.FloatProperty( name="Letter Spacing", description="Letter Spacing", + update=update_callback, ) class ABC3D_text_properties(bpy.types.PropertyGroup): @@ -564,6 +579,95 @@ class ABC3D_PT_TextManagement(bpy.types.Panel): layout.row().operator(f"{__name__}.remove_text", text="Remove Textobject") +class ABC3D_PG_FontCreation(bpy.types.PropertyGroup): + bl_label = "Font Creation Properties" + # bl_parent_id = "ABC3D_PG_NamingHelper" + # bl_category = "ABC3D" + # bl_space_type = "VIEW_3D" + # bl_region_type = "UI" + # bl_options = {"DEFAULT_CLOSED"} + + def naming_glyph_id_update_callback(self, context): + glyph_name = Font.glyph_to_name(self.naming_glyph_id) + if self.naming_glyph_full: + self.naming_glyph_name = f"{glyph_name}_{self.font_name}_{self.face_name}" + else: + self.naming_glyph_name = glyph_name + + naming_glyph_id: bpy.props.StringProperty( + name="", + description="find proper naming for a glyph", + default="", + maxlen=32, + update=naming_glyph_id_update_callback, + ) + + naming_glyph_name: bpy.props.StringProperty( + name="", + description="find proper naming for a glyph", + default="", + maxlen=1024, + ) + + naming_glyph_full: bpy.props.BoolProperty( + default=True, + description="Generate full name", + update=naming_glyph_id_update_callback, + ) + + font_name: bpy.props.StringProperty( + name="", + description="Font name", + default="NM_Origin", + update=naming_glyph_id_update_callback, + ) + + face_name: bpy.props.StringProperty( + name="", + description="FontFace name", + default="Tender", + 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" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + abc3d_font_creation = context.scene.abc3d_font_creation + name = abc3d_font_creation.naming_glyph_name + context.active_object.name = name + return {"FINISHED"} + + +class ABC3D_PT_NamingHelper(bpy.types.Panel): + bl_label = "Naming Helper" + bl_parent_id = "ABC3D_PT_FontCreation" + bl_category = "ABC3D" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_options = {"DEFAULT_CLOSED"} + + def draw(self, context): + layout = self.layout + scene = context.scene + + abc3d_font_creation = scene.abc3d_font_creation + + box = layout.box() + box.label(text="Glyph Naming Helper") + box.row().prop(abc3d_font_creation, "naming_glyph_full") + box.label(text="Glyph Character Input") + box.row().prop(abc3d_font_creation, "naming_glyph_id") + box.label(text="Font name:") + box.row().prop(abc3d_font_creation, "font_name") + box.label(text="FontFace name:") + 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") + class ABC3D_PT_FontCreation(bpy.types.Panel): bl_label = "Font Creation" bl_parent_id = "ABC3D_PT_Panel" @@ -575,9 +679,6 @@ class ABC3D_PT_FontCreation(bpy.types.Panel): def draw(self, context): layout = self.layout wm = context.window_manager - scene = context.scene - - abc3d_data = scene.abc3d_data layout.row().operator( f"{__name__}.toggle_abc3d_collection", text="Toggle Collection" @@ -588,6 +689,7 @@ class ABC3D_PT_FontCreation(bpy.types.Panel): layout.row().operator( f"{__name__}.save_font_to_file", text="Export Font To File" ) + box = layout.box() box.label(text="metrics") box.row().operator( @@ -602,6 +704,10 @@ class ABC3D_PT_FontCreation(bpy.types.Panel): layout.row().operator( 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_metrics", text="Align origins to Metrics (left)") + # box.row().operator(f"{__name__}.fix_objects_metrics_origins", text="Fix objects metrics origins") class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): @@ -613,14 +719,38 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): def get_active_text_properties(self): # and bpy.context.object.select_get(): - if type(bpy.context.active_object) != type(None): - for t in bpy.context.scene.abc3d_data.available_texts: - if bpy.context.active_object == t.text_object: - return t - if bpy.context.active_object.parent == t.text_object: - return t + a_o = bpy.context.active_object + if a_o is not None: + 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 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 + # NOTE: HERE + def get_active_glyph_properties(self): + a_o = bpy.context.active_object + if a_o is not None: + 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"] + return bpy.context.scene.abc3d_data.available_texts[text_index].glyphs[glyph_index] + else: + 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() @@ -650,7 +780,7 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): @classmethod def poll(self, context): - return type(self.get_active_text_properties(self)) != type(None) + return self.get_active_text_properties(self) is not None def draw(self, context): layout = self.layout @@ -659,11 +789,16 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): abc3d_data = scene.abc3d_data props = self.get_active_text_properties() + glyph_props = self.get_active_glyph_properties() - if type(props) == type(None) or type(props.text_object) == type(None): + if props is None or props.text_object is None: # this should not happen # as then polling does not work # however, we are paranoid + if props is None: + layout.label(text="props is none") + elif props.text_object is None: + layout.label(text="props.text_object is none") return layout.label(text=f"Mom: {props.text_object.name}") @@ -677,6 +812,13 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): layout.column().prop(props, "translation") layout.column().prop(props, "orientation") + if glyph_props is None: + return + box = layout.box() + box.label(text=f"{glyph_props.glyph_id}") + box.row().prop(glyph_props, "letter_spacing") + + class ABC3D_OT_InstallFont(bpy.types.Operator): """Install or load Fontfile from path above. @@ -923,6 +1065,72 @@ 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.""" + + bl_idname = f"{__name__}.align_origins_to_active_object" + 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') + + 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. + + # 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.""" + + # 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, + # ) + + # 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 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. + + # 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"} 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.""" @@ -1517,6 +1725,9 @@ classes = ( ABC3D_PT_TextPlacement, ABC3D_PT_TextManagement, ABC3D_PT_FontCreation, + ABC3D_PG_FontCreation, + ABC3D_OT_NamingHelper, + ABC3D_PT_NamingHelper, ABC3D_PT_TextPropertiesPanel, ABC3D_OT_OpenAssetDirectory, ABC3D_OT_LoadInstalledFonts, @@ -1525,6 +1736,9 @@ classes = ( ABC3D_OT_RemoveMetrics, ABC3D_OT_AlignMetricsToActiveObject, ABC3D_OT_AlignMetrics, + ABC3D_OT_AlignOriginsToActiveObject, + # ABC3D_OT_AlignOriginsToMetrics, + # ABC3D_OT_FixObjectsMetricsOrigins, ABC3D_OT_TemporaryHelper, ABC3D_OT_RemoveText, ABC3D_OT_PlaceText, @@ -1673,6 +1887,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.Object.__del__ = lambda self: print(f"Bye {self.name}") # autostart if we load a blend file @@ -1716,6 +1931,7 @@ def unregister(): bpy.app.handlers.depsgraph_update_post.remove(on_depsgraph_update) del bpy.types.Scene.abc3d_data + del bpy.types.Scene.abc3d_font_creation print(f"UNREGISTER {utils.prefix()}") diff --git a/_vimrc_local.vim b/_vimrc_local.vim deleted file mode 100644 index cac1787..0000000 --- a/_vimrc_local.vim +++ /dev/null @@ -1,20 +0,0 @@ -""""""""""""""""""""""""""""""""" JEDI - -let g:jedi#auto_initialization = 1 -let g:jedi#use_tabs_not_buffers = 1 -let g:jedi#environment_path = "venv" - -""""""""""""""""""""""""""""""""" ALE - -"let g:ale_python_pylint_executable = '/home/jrkb/git/pointer/neomatter/font3d/abc3d/venv/bin/pylint' -"let g:ale_python_executable='/home/jrkb/git/pointer/neomatter/font3d/abc3d/venv/bin/python' -"let g:ale_python_pylint_use_global=1 -"let g:ale_use_global_executables=1 -"let g:ale_python_auto_pipenv=1 -"let g:ale_python_auto_virtualenv=1 -"let g:ale_virtualenv_dir_names = ['venv'] - -"let g:ale_linters = { 'javascript': ['eslint', 'tsserver'], 'python': ['jedils', 'pylint', 'flake8'], 'cpp': ['cc', 'clangcheck', 'clangd', 'clangtidy', 'clazy', 'cppcheck', 'cpplint', 'cquery', 'cspell', 'flawfinder'], 'php': ['php_cs_fixer'] } -"let g:ale_fixers = { '*': ['remove_trailing_lines', 'trim_whitespace'], 'python': ['autopep8'], 'cpp': ['uncrustify'], 'javascript': js_fixers, 'css': ['prettier'], 'json': ['prettier'], 'php': ['php_cs_fixer'] } - -"let g:ale_pattern_options = {'\.py$': {'ale_enabled': 0}} diff --git a/butils.py b/butils.py index 9ddfba1..94ed804 100644 --- a/butils.py +++ b/butils.py @@ -485,7 +485,7 @@ def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""): modified_font_faces = [] all_glyph_os = [] - all_objects = [] + for o in bpy.context.scene.objects: if marker_property in o: if "type" in o and o["type"] == "glyph": @@ -511,17 +511,17 @@ def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""): modified_font_faces.append({"font_name": font_name, "face_name": face_name}) for mff in modified_font_faces: - glyphs = [] + mff_glyphs = [] face = Font.fonts[mff["font_name"]].faces[mff["face_name"]] # iterate glyphs for g in face.glyphs: # iterate alternates for glyph in face.glyphs[g]: - glyphs.append(get_original(glyph)) - if len(glyphs) > 0: - add_default_metrics_to_objects(glyphs) + mff_glyphs.append(get_original(glyph)) + if len(mff_glyphs) > 0: + add_default_metrics_to_objects(mff_glyphs) # calculate unit factor - h = get_glyph_height(glyphs[0]) + h = get_glyph_height(mff_glyphs[0]) if h != 0: face.unit_factor = 1 / h @@ -733,6 +733,12 @@ 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): + return -1 * c.bound_box[0][0], c.bound_box[4][0] + return -1 * glyph_obj.bound_box[0][0], glyph_obj.bound_box[4][0] + def get_glyph_height(glyph_obj): for c in glyph_obj.children: @@ -802,6 +808,41 @@ def will_regenerate(text_properties): return False +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 + +def is_or_has_parent(o, parent, if_is_parent=True, max_depth=10): + if o == parent and if_is_parent: + return True + for i in range(0, max_depth): + o = o.parent + if o == parent: + return True + if o is None: + return False + return False + +def parent_to_curve(o, c): + 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': + cm = c.to_mesh() + p = cm.vertices[0].co + o.matrix_parent_inverse.translation = p * -1.0 + def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4): """set_text_on_curve @@ -815,8 +856,9 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) :param reset_depsgraph_n: reset external parameters after n-th depsgraph update. (<= 0) = immediate, (> 0) = reset after n-th depsgraph update, (False) = no depsgraph reset :type reset_depsgraph_n: int """ - - global lock_depsgraph_update_n_times + # NOTE: depsgraph update not locked + # as we fixed data_path with parent_to_curve trick + # global lock_depsgraph_update_n_times # starttime = time.perf_counter_ns() mom = text_properties.text_object @@ -825,28 +867,44 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) distribution_type = "CALCULATE" if is_bezier(mom) else "FOLLOW_PATH" + # 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 + # 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 = will_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 glyph_advance = 0 + glyph_index = 0 is_command = False previous_spline_index = -1 + for i, c in enumerate(text_properties.text): face = Font.fonts[text_properties.font_name].faces[text_properties.face_name] scalor = face.unit_factor * text_properties.font_size @@ -859,7 +917,6 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) is_newline = True next_line_advance = get_next_line_advance(mom, advance, glyph_advance) if advance == next_line_advance: - # self.report({'INFO'}, f"would like to add new line for {text_properties.text} please") print( f"would like to add new line for {text_properties.text} please" ) @@ -869,9 +926,14 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) is_command = False glyph_id = c - glyph_tmp = Font.get_glyph( - text_properties.font_name, text_properties.face_name, glyph_id - ) + spline_index = 0 + + ############### GET GLYPH + + 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: @@ -886,6 +948,7 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) text_properties.font_name, text_properties.face_name, possible_replacement, + -1 ) if glyph_tmp is not None: message = message + f" (replaced with '{possible_replacement}')" @@ -899,61 +962,85 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) ) if not replaced: continue + glyph = glyph_tmp.original - ob = None - obg = None + ############### GLYPH PROPERTIES + + glyph_properties = text_properties.glyphs[glyph_index] if not regenerate else text_properties.glyphs.add() + if regenerate: - ob = bpy.data.objects.new(f"{glyph_id}", None) - ob.hide_viewport = True - obg = bpy.data.objects.new(f"{glyph_id}_mesh", glyph.data) - ob[f"{utils.prefix()}_type"] = "glyph" - ob[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id - ob[f"{utils.prefix()}_font_name"] = text_properties.font_name - ob[f"{utils.prefix()}_face_name"] = text_properties.face_name + glyph_properties["glyph_id"] = glyph_id + glyph_properties["text_id"] = text_properties.text_id + glyph_properties["letter_spacing"] = 0 + + ############### NODE SCENE MANAGEMENT + + 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) + + glyph_properties["glyph_object"] = outer_node else: - ob = text_properties.glyphs[i].glyph_object - for c in ob.children: + 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"): - obg = c + inner_node = c + + ############### TRANSFORMS + + # origins could be shifted + # so we need to apply a pre_advance + glyph_pre_advance, glyph_post_advance = get_glyph_prepost_advances(glyph) + advance += glyph_pre_advance * scalor if distribution_type == "FOLLOW_PATH": - ob.constraints.new(type="FOLLOW_PATH") - ob.constraints["Follow Path"].target = mom - ob.constraints["Follow Path"].use_fixed_location = True - ob.constraints["Follow Path"].offset_factor = advance / curve_length - ob.constraints["Follow Path"].use_curve_follow = True - ob.constraints["Follow Path"].forward_axis = "FORWARD_X" - ob.constraints["Follow Path"].up_axis = "UP_Y" + 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 = 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_ob_rotation_mode = None - previous_obg_rotation_mode = None - if ob.rotation_mode != "QUATERNION": - ob.rotation_mode = "QUATERNION" - previous_ob_rotation_mode = ob.rotation_mode - if obg.rotation_mode != "QUATERNION": - obg.rotation_mode = "QUATERNION" - previous_obg_rotation_mode = obg.rotation_mode + 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 - location, tangent, spline_index = calc_point_on_bezier_curve( - mom, advance, True, True - ) + # get info from bezier + location, tangent, spline_index = calc_point_on_bezier_curve(mom, advance, True, True) + + # check if we are on a new line if spline_index != previous_spline_index: is_newline = True - if regenerate: - # ob.location = mom.matrix_world @ ( - # location + text_properties.translation - # ) - ob.location = location + text_properties.translation - mom.users_collection[0].objects.link(obg) - mom.users_collection[0].objects.link(ob) - ob.parent = mom - obg.parent = ob - obg.location = mathutils.Vector((0.0, 0.0, 0.0)) - else: - ob.location = location + text_properties.translation + # position + outer_node.location = location + text_properties.translation # orientation / rotation mask = [0] @@ -967,26 +1054,28 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) q = mathutils.Quaternion() q.rotate(text_properties.orientation) - ob.rotation_quaternion = (motor[0].to_3x3() @ q.to_matrix()).to_quaternion() - # if regenerate: - # obg.rotation_quaternion = q - # ob.rotation_quaternion = ( - # mom.matrix_world @ motor[0] - # ).to_quaternion() - # else: - # ob.rotation_quaternion = motor[0].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: - # ob.rotation_quaternion = (mom.matrix_world.inverted().to_3x3() @ 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: + # outer_node.rotation_quaternion = (mom.matrix_world.inverted().to_3x3() @ motor[0].to_3x3() @ q.to_matrix()).to_quaternion() - if previous_ob_rotation_mode: - ob.rotation_mode = previous_ob_rotation_mode - if previous_obg_rotation_mode: - obg.rotation_mode = previous_obg_rotation_mode + # # scale + outer_node.scale = (scalor, scalor, scalor) + 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 + outer_node.hide_set(True) + + ############### PREPARE FOR THE NEXT + + print(f"{glyph_id}: {glyph_properties.letter_spacing=}") glyph_advance = ( - get_glyph_advance(glyph) * scalor + text_properties.letter_spacing + glyph_post_advance * scalor + text_properties.letter_spacing + glyph_properties.letter_spacing ) # now we need to compensate for curvature @@ -995,7 +1084,7 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) curve_compensation = 0 if distribution_type == "CALCULATE" and ( not is_newline or spline_index == 0 - ): # TODO: fix newline hack + ): if text_properties.compensate_curvature and glyph_advance > 0: previous_location, psi = calc_point_on_bezier_curve( mom, advance, False, True @@ -1025,67 +1114,46 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4) output_spline_index=True, ) - ob.scale = (scalor, scalor, scalor) - advance = advance + glyph_advance + curve_compensation + glyph_index += 1 previous_spline_index = spline_index - if regenerate: - glyph_data = text_properties.glyphs.add() - glyph_data.glyph_id = glyph_id - glyph_data.glyph_object = ob - glyph_data.letter_spacing = 0 - - if regenerate: - 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 - - 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) + # 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 @@ -1502,3 +1570,109 @@ 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 + if len(objects) == 0: + return "no objects selected" + + if bpy.context.active_object is None: + return "no active object selected" + + reference_origin_position = bpy.context.active_object.matrix_world.translation[axis] + + # do it + for o in objects: + is_possibly_glyph = is_glyph(o) + if is_possibly_glyph and o is not bpy.context.active_object: + if is_mesh(o): + diff = reference_origin_position - o.matrix_world.translation[axis] + + for v in o.data.vertices: + 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)]) + +# 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 + +# 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 + + +# 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" + + # 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" + + # 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 + + # diff = metrics_origin_x - o.matrix_world.translation[0] + + # for v in o.data.vertices: + # v.co[0] -= diff + + # 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() + + # return "" diff --git a/common/Font.py b/common/Font.py index f01c91b..bcb7949 100644 --- a/common/Font.py +++ b/common/Font.py @@ -1,6 +1,4 @@ -from typing import TypedDict from typing import Dict -from dataclasses import dataclass from pathlib import Path # convenience dictionary for translating names to glyph ids @@ -45,9 +43,9 @@ known_misspellings = { "overdot": "dotaccent", "diaresis": "dieresis", "diaeresis": "dieresis", + # different conventions + "doubleacute": "hungarumlaut", # character does not exist.. maybe something else - "Odoubleacute": "Ohungarumlaut", - "Udoubleacute": "Uhungarumlaut", "Wcaron": "Wcircumflex", "Neng": "Nlongrightleg", "Lgrave": "Lacute", @@ -77,6 +75,13 @@ def name_to_glyph(name): return None +def glyph_to_name(glyph_id): + for k in name_to_glyph_d: + if glyph_id == name_to_glyph_d[k]: + return k + return glyph_id + + def is_space(character): for name in space_d: if character == space_d[name][0]: diff --git a/common/utils.py b/common/utils.py index 0c99dfd..f2fe8d0 100644 --- a/common/utils.py +++ b/common/utils.py @@ -8,7 +8,7 @@ def get_version_minor(): def get_version_patch(): - return 5 + return 6 def get_version_string(): @@ -22,7 +22,6 @@ def prefix(): import datetime import time -from mathutils import Vector def get_timestamp():