Compare commits

..

No commits in common. "56afa0b453b0bf2b0fbdc1f2237b6db79923f6db" and "71dda9f316c34f99fc1f0b94600915b2b381a455" have entirely different histories.

6 changed files with 174 additions and 548 deletions

View file

@ -5,7 +5,7 @@
/ ___ \| |_) | |___ ___) | |_| | / ___ \| |_) | |___ ___) | |_| |
/_/ \_\____/ \____|____/|____/ /_/ \_\____/ \____|____/|____/
``` ```
v0.0.6 v0.0.5
Convenience tool to work with 3D typography in Blender and Cinema4D. Convenience tool to work with 3D typography in Blender and Cinema4D.

View file

@ -16,13 +16,13 @@ from .common import Font, utils
bl_info = { bl_info = {
"name": "ABC3D", "name": "ABC3D",
"author": "Jakob Schlötter, Studio Pointer*", "author": "Jakob Schlötter, Studio Pointer*",
"version": (0, 0, 6), "version": (0, 0, 5),
"blender": (4, 1, 0), "blender": (4, 1, 0),
"location": "VIEW3D", "location": "VIEW3D",
"description": "Convenience addon for 3D fonts", "description": "Convenience addon for 3D fonts",
"category": "Typography", "category": "Typography",
} }
# NOTE: also change version in common/utils.py and README.md # NOTE: also change version in common/utils.py
# make sure that modules are reloadable # make sure that modules are reloadable
# when registering # when registering
@ -134,26 +134,11 @@ class ABC3D_available_font(bpy.types.PropertyGroup):
class ABC3D_glyph_properties(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) 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) glyph_object: bpy.props.PointerProperty(type=bpy.types.Object)
letter_spacing: bpy.props.FloatProperty( letter_spacing: bpy.props.FloatProperty(
name="Letter Spacing", name="Letter Spacing",
description="Letter Spacing", description="Letter Spacing",
update=update_callback,
) )
class ABC3D_text_properties(bpy.types.PropertyGroup): class ABC3D_text_properties(bpy.types.PropertyGroup):
@ -579,95 +564,6 @@ class ABC3D_PT_TextManagement(bpy.types.Panel):
layout.row().operator(f"{__name__}.remove_text", text="Remove Textobject") 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): class ABC3D_PT_FontCreation(bpy.types.Panel):
bl_label = "Font Creation" bl_label = "Font Creation"
bl_parent_id = "ABC3D_PT_Panel" bl_parent_id = "ABC3D_PT_Panel"
@ -679,6 +575,9 @@ class ABC3D_PT_FontCreation(bpy.types.Panel):
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
wm = context.window_manager wm = context.window_manager
scene = context.scene
abc3d_data = scene.abc3d_data
layout.row().operator( layout.row().operator(
f"{__name__}.toggle_abc3d_collection", text="Toggle Collection" f"{__name__}.toggle_abc3d_collection", text="Toggle Collection"
@ -689,7 +588,6 @@ class ABC3D_PT_FontCreation(bpy.types.Panel):
layout.row().operator( layout.row().operator(
f"{__name__}.save_font_to_file", text="Export Font To File" f"{__name__}.save_font_to_file", text="Export Font To File"
) )
box = layout.box() box = layout.box()
box.label(text="metrics") box.label(text="metrics")
box.row().operator( box.row().operator(
@ -704,10 +602,6 @@ class ABC3D_PT_FontCreation(bpy.types.Panel):
layout.row().operator( layout.row().operator(
f"{__name__}.temporaryhelper", text="Debug Function Do Not Use" 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): class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel):
@ -719,38 +613,14 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel):
def get_active_text_properties(self): def get_active_text_properties(self):
# and bpy.context.object.select_get(): # and bpy.context.object.select_get():
a_o = bpy.context.active_object if type(bpy.context.active_object) != type(None):
if a_o is not None: for t in bpy.context.scene.abc3d_data.available_texts:
if f"{utils.prefix()}_linked_textobject" in a_o: if bpy.context.active_object == t.text_object:
text_index = a_o[f"{utils.prefix()}_linked_textobject"] return t
return bpy.context.scene.abc3d_data.available_texts[text_index] if bpy.context.active_object.parent == t.text_object:
elif f"{utils.prefix()}_linked_textobject" in a_o.parent: return t
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 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): # def font_items_callback(self, context):
# items = [] # items = []
# fonts = Font.get_loaded_fonts_and_faces() # fonts = Font.get_loaded_fonts_and_faces()
@ -780,7 +650,7 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel):
@classmethod @classmethod
def poll(self, context): def poll(self, context):
return self.get_active_text_properties(self) is not None return type(self.get_active_text_properties(self)) != type(None)
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
@ -789,16 +659,11 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel):
abc3d_data = scene.abc3d_data abc3d_data = scene.abc3d_data
props = self.get_active_text_properties() props = self.get_active_text_properties()
glyph_props = self.get_active_glyph_properties()
if props is None or props.text_object is None: if type(props) == type(None) or type(props.text_object) == type(None):
# this should not happen # this should not happen
# as then polling does not work # as then polling does not work
# however, we are paranoid # 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 return
layout.label(text=f"Mom: {props.text_object.name}") layout.label(text=f"Mom: {props.text_object.name}")
@ -812,13 +677,6 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel):
layout.column().prop(props, "translation") layout.column().prop(props, "translation")
layout.column().prop(props, "orientation") 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): class ABC3D_OT_InstallFont(bpy.types.Operator):
"""Install or load Fontfile from path above. """Install or load Fontfile from path above.
@ -1065,72 +923,6 @@ class ABC3D_OT_AlignMetrics(bpy.types.Operator):
butils.align_metrics_of_objects(objects) butils.align_metrics_of_objects(objects)
return {"FINISHED"} 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): 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.""" """Temporary Helper ABC3D\nThis could do anything.\nIt's just there to make random functions available for testing."""
@ -1725,9 +1517,6 @@ classes = (
ABC3D_PT_TextPlacement, ABC3D_PT_TextPlacement,
ABC3D_PT_TextManagement, ABC3D_PT_TextManagement,
ABC3D_PT_FontCreation, ABC3D_PT_FontCreation,
ABC3D_PG_FontCreation,
ABC3D_OT_NamingHelper,
ABC3D_PT_NamingHelper,
ABC3D_PT_TextPropertiesPanel, ABC3D_PT_TextPropertiesPanel,
ABC3D_OT_OpenAssetDirectory, ABC3D_OT_OpenAssetDirectory,
ABC3D_OT_LoadInstalledFonts, ABC3D_OT_LoadInstalledFonts,
@ -1736,9 +1525,6 @@ classes = (
ABC3D_OT_RemoveMetrics, ABC3D_OT_RemoveMetrics,
ABC3D_OT_AlignMetricsToActiveObject, ABC3D_OT_AlignMetricsToActiveObject,
ABC3D_OT_AlignMetrics, ABC3D_OT_AlignMetrics,
ABC3D_OT_AlignOriginsToActiveObject,
# ABC3D_OT_AlignOriginsToMetrics,
# ABC3D_OT_FixObjectsMetricsOrigins,
ABC3D_OT_TemporaryHelper, ABC3D_OT_TemporaryHelper,
ABC3D_OT_RemoveText, ABC3D_OT_RemoveText,
ABC3D_OT_PlaceText, ABC3D_OT_PlaceText,
@ -1887,7 +1673,6 @@ def register():
addon_updater_ops.make_annotations(cls) # Avoid blender 2.8 warnings. addon_updater_ops.make_annotations(cls) # Avoid blender 2.8 warnings.
bpy.utils.register_class(cls) bpy.utils.register_class(cls)
bpy.types.Scene.abc3d_data = bpy.props.PointerProperty(type=ABC3D_data) 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}") # bpy.types.Object.__del__ = lambda self: print(f"Bye {self.name}")
# autostart if we load a blend file # autostart if we load a blend file
@ -1931,7 +1716,6 @@ def unregister():
bpy.app.handlers.depsgraph_update_post.remove(on_depsgraph_update) bpy.app.handlers.depsgraph_update_post.remove(on_depsgraph_update)
del bpy.types.Scene.abc3d_data del bpy.types.Scene.abc3d_data
del bpy.types.Scene.abc3d_font_creation
print(f"UNREGISTER {utils.prefix()}") print(f"UNREGISTER {utils.prefix()}")

20
_vimrc_local.vim Normal file
View file

@ -0,0 +1,20 @@
""""""""""""""""""""""""""""""""" 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}}

442
butils.py
View file

@ -485,7 +485,7 @@ def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""):
modified_font_faces = [] modified_font_faces = []
all_glyph_os = [] all_glyph_os = []
all_objects = []
for o in bpy.context.scene.objects: for o in bpy.context.scene.objects:
if marker_property in o: if marker_property in o:
if "type" in o and o["type"] == "glyph": 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}) modified_font_faces.append({"font_name": font_name, "face_name": face_name})
for mff in modified_font_faces: for mff in modified_font_faces:
mff_glyphs = [] glyphs = []
face = Font.fonts[mff["font_name"]].faces[mff["face_name"]] face = Font.fonts[mff["font_name"]].faces[mff["face_name"]]
# iterate glyphs # iterate glyphs
for g in face.glyphs: for g in face.glyphs:
# iterate alternates # iterate alternates
for glyph in face.glyphs[g]: for glyph in face.glyphs[g]:
mff_glyphs.append(get_original(glyph)) glyphs.append(get_original(glyph))
if len(mff_glyphs) > 0: if len(glyphs) > 0:
add_default_metrics_to_objects(mff_glyphs) add_default_metrics_to_objects(glyphs)
# calculate unit factor # calculate unit factor
h = get_glyph_height(mff_glyphs[0]) h = get_glyph_height(glyphs[0])
if h != 0: if h != 0:
face.unit_factor = 1 / h face.unit_factor = 1 / h
@ -733,12 +733,6 @@ def get_glyph_advance(glyph_obj):
return abs(c.bound_box[4][0] - c.bound_box[0][0]) 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]) 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): def get_glyph_height(glyph_obj):
for c in glyph_obj.children: for c in glyph_obj.children:
@ -808,41 +802,6 @@ def will_regenerate(text_properties):
return False 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): def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4):
"""set_text_on_curve """set_text_on_curve
@ -856,9 +815,8 @@ 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 :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 :type reset_depsgraph_n: int
""" """
# NOTE: depsgraph update not locked
# as we fixed data_path with parent_to_curve trick global lock_depsgraph_update_n_times
# global lock_depsgraph_update_n_times
# starttime = time.perf_counter_ns() # starttime = time.perf_counter_ns()
mom = text_properties.text_object mom = text_properties.text_object
@ -867,44 +825,28 @@ 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" 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 # use_path messes with parenting
# however, we need it for follow_path # however, we need it for follow_path
# https://projects.blender.org/blender/blender/issues/100661 # https://projects.blender.org/blender/blender/issues/100661
# previous_use_path = mom.data.use_path previous_use_path = mom.data.use_path
# if distribution_type == "CALCULATE": if distribution_type == "CALCULATE":
# mom.data.use_path = False mom.data.use_path = False
# elif distribution_type == "FOLLOW_PATH": elif distribution_type == "FOLLOW_PATH":
# mom.data.use_path = True mom.data.use_path = True
regenerate = will_regenerate(text_properties) regenerate = will_regenerate(text_properties)
# if we regenerate.... delete objects # if we regenerate.... delete objects
if regenerate and text_properties.get("glyphs"): 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"]] glyph_objects = [g["glyph_object"] for g in text_properties["glyphs"]]
completely_delete_objects(glyph_objects, True) completely_delete_objects(glyph_objects, True)
text_properties.glyphs.clear() 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) curve_length = get_curve_length(mom)
advance = text_properties.offset advance = text_properties.offset
glyph_advance = 0 glyph_advance = 0
glyph_index = 0
is_command = False is_command = False
previous_spline_index = -1 previous_spline_index = -1
for i, c in enumerate(text_properties.text): for i, c in enumerate(text_properties.text):
face = Font.fonts[text_properties.font_name].faces[text_properties.face_name] face = Font.fonts[text_properties.font_name].faces[text_properties.face_name]
scalor = face.unit_factor * text_properties.font_size scalor = face.unit_factor * text_properties.font_size
@ -917,6 +859,7 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4)
is_newline = True is_newline = True
next_line_advance = get_next_line_advance(mom, advance, glyph_advance) next_line_advance = get_next_line_advance(mom, advance, glyph_advance)
if advance == next_line_advance: if advance == next_line_advance:
# self.report({'INFO'}, f"would like to add new line for {text_properties.text} please")
print( print(
f"would like to add new line for {text_properties.text} please" f"would like to add new line for {text_properties.text} please"
) )
@ -926,14 +869,9 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4)
is_command = False is_command = False
glyph_id = c glyph_id = c
spline_index = 0 glyph_tmp = Font.get_glyph(
text_properties.font_name, text_properties.face_name, glyph_id
############### GET GLYPH )
glyph_tmp = Font.get_glyph(text_properties.font_name,
text_properties.face_name,
glyph_id,
-1)
if glyph_tmp is None: if glyph_tmp is None:
space_width = Font.is_space(glyph_id) space_width = Font.is_space(glyph_id)
if space_width: if space_width:
@ -948,7 +886,6 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4)
text_properties.font_name, text_properties.font_name,
text_properties.face_name, text_properties.face_name,
possible_replacement, possible_replacement,
-1
) )
if glyph_tmp is not None: if glyph_tmp is not None:
message = message + f" (replaced with '{possible_replacement}')" message = message + f" (replaced with '{possible_replacement}')"
@ -962,85 +899,61 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4)
) )
if not replaced: if not replaced:
continue continue
glyph = glyph_tmp.original glyph = glyph_tmp.original
############### GLYPH PROPERTIES ob = None
obg = None
glyph_properties = text_properties.glyphs[glyph_index] if not regenerate else text_properties.glyphs.add()
if regenerate: if regenerate:
glyph_properties["glyph_id"] = glyph_id ob = bpy.data.objects.new(f"{glyph_id}", None)
glyph_properties["text_id"] = text_properties.text_id ob.hide_viewport = True
glyph_properties["letter_spacing"] = 0 obg = bpy.data.objects.new(f"{glyph_id}_mesh", glyph.data)
ob[f"{utils.prefix()}_type"] = "glyph"
############### NODE SCENE MANAGEMENT ob[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id
ob[f"{utils.prefix()}_font_name"] = text_properties.font_name
inner_node = None ob[f"{utils.prefix()}_face_name"] = text_properties.face_name
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: else:
outer_node = glyph_properties.glyph_object ob = text_properties.glyphs[i].glyph_object
outer_node[f"{utils.prefix()}_glyph_index"] = glyph_index for c in ob.children:
for c in outer_node.children:
if c.name.startswith(f"{glyph_id}_mesh"): if c.name.startswith(f"{glyph_id}_mesh"):
inner_node = c obg = 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": if distribution_type == "FOLLOW_PATH":
outer_node.constraints.new(type="FOLLOW_PATH") ob.constraints.new(type="FOLLOW_PATH")
outer_node.constraints["Follow Path"].target = mom ob.constraints["Follow Path"].target = mom
outer_node.constraints["Follow Path"].use_fixed_location = True ob.constraints["Follow Path"].use_fixed_location = True
outer_node.constraints["Follow Path"].offset_factor = advance / curve_length ob.constraints["Follow Path"].offset_factor = advance / curve_length
outer_node.constraints["Follow Path"].use_curve_follow = True ob.constraints["Follow Path"].use_curve_follow = True
outer_node.constraints["Follow Path"].forward_axis = "FORWARD_X" ob.constraints["Follow Path"].forward_axis = "FORWARD_X"
outer_node.constraints["Follow Path"].up_axis = "UP_Y" ob.constraints["Follow Path"].up_axis = "UP_Y"
spline_index = 0 spline_index = 0
elif distribution_type == "CALCULATE": elif distribution_type == "CALCULATE":
previous_outer_node_rotation_mode = None previous_ob_rotation_mode = None
previous_inner_node_rotation_mode = None previous_obg_rotation_mode = None
if outer_node.rotation_mode != "QUATERNION": if ob.rotation_mode != "QUATERNION":
outer_node.rotation_mode = "QUATERNION" ob.rotation_mode = "QUATERNION"
previous_outer_node_rotation_mode = outer_node.rotation_mode previous_ob_rotation_mode = ob.rotation_mode
if inner_node.rotation_mode != "QUATERNION": if obg.rotation_mode != "QUATERNION":
inner_node.rotation_mode = "QUATERNION" obg.rotation_mode = "QUATERNION"
previous_inner_node_rotation_mode = inner_node.rotation_mode previous_obg_rotation_mode = obg.rotation_mode
# get info from bezier location, tangent, spline_index = calc_point_on_bezier_curve(
location, tangent, spline_index = calc_point_on_bezier_curve(mom, advance, True, True) mom, advance, True, True
)
# check if we are on a new line
if spline_index != previous_spline_index: if spline_index != previous_spline_index:
is_newline = True is_newline = True
# position if regenerate:
outer_node.location = location + text_properties.translation # 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
# orientation / rotation # orientation / rotation
mask = [0] mask = [0]
@ -1054,28 +967,26 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4)
q = mathutils.Quaternion() q = mathutils.Quaternion()
q.rotate(text_properties.orientation) q.rotate(text_properties.orientation)
outer_node.rotation_quaternion = (motor[0].to_3x3() @ q.to_matrix()).to_quaternion() 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()
# # NOTE: supercool but out of scope, as we wouldhave to update it everytime the curve object rotates, # 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: # 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() # ob.rotation_quaternion = (mom.matrix_world.inverted().to_3x3() @ motor[0].to_3x3() @ q.to_matrix()).to_quaternion()
# # scale if previous_ob_rotation_mode:
outer_node.scale = (scalor, scalor, scalor) ob.rotation_mode = previous_ob_rotation_mode
if previous_obg_rotation_mode:
obg.rotation_mode = previous_obg_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
outer_node.hide_set(True)
############### PREPARE FOR THE NEXT
print(f"{glyph_id}: {glyph_properties.letter_spacing=}")
glyph_advance = ( glyph_advance = (
glyph_post_advance * scalor + text_properties.letter_spacing + glyph_properties.letter_spacing get_glyph_advance(glyph) * scalor + text_properties.letter_spacing
) )
# now we need to compensate for curvature # now we need to compensate for curvature
@ -1084,7 +995,7 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4)
curve_compensation = 0 curve_compensation = 0
if distribution_type == "CALCULATE" and ( if distribution_type == "CALCULATE" and (
not is_newline or spline_index == 0 not is_newline or spline_index == 0
): ): # TODO: fix newline hack
if text_properties.compensate_curvature and glyph_advance > 0: if text_properties.compensate_curvature and glyph_advance > 0:
previous_location, psi = calc_point_on_bezier_curve( previous_location, psi = calc_point_on_bezier_curve(
mom, advance, False, True mom, advance, False, True
@ -1114,46 +1025,67 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4)
output_spline_index=True, output_spline_index=True,
) )
ob.scale = (scalor, scalor, scalor)
advance = advance + glyph_advance + curve_compensation advance = advance + glyph_advance + curve_compensation
glyph_index += 1
previous_spline_index = spline_index previous_spline_index = spline_index
# NOTE: depsgraph update not locked if regenerate:
# as we fixed data_path with parent_to_curve trick glyph_data = text_properties.glyphs.add()
# if lock_depsgraph_update_n_times < 0: glyph_data.glyph_id = glyph_id
# lock_depsgraph_update_n_times = len( glyph_data.glyph_object = ob
# bpy.context.selected_objects glyph_data.letter_spacing = 0
# )
# else: if regenerate:
# lock_depsgraph_update_n_times += len( mom[f"{utils.prefix()}_type"] = "textobject"
# bpy.context.selected_objects mom[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id
# ) mom[f"{utils.prefix()}_font_name"] = text_properties.font_name
# # NOTE: we reset with a timeout, as setting and resetting certain things mom[f"{utils.prefix()}_face_name"] = text_properties.face_name
# # in fast succession will cause visual glitches (e.g. {}.data.use_path). mom[f"{utils.prefix()}_font_size"] = text_properties.font_size
# def reset(): mom[f"{utils.prefix()}_letter_spacing"] = text_properties.letter_spacing
# mom.data.use_path = previous_use_path mom[f"{utils.prefix()}_orientation"] = text_properties.orientation
# if counted_reset in bpy.app.handlers.depsgraph_update_post: mom[f"{utils.prefix()}_translation"] = text_properties.translation
# bpy.app.handlers.depsgraph_update_post.remove(counted_reset)
# if bpy.app.timers.is_registered(reset): if lock_depsgraph_update_n_times < 0:
# bpy.app.timers.unregister(reset) lock_depsgraph_update_n_times = len(
# molotov = reset_depsgraph_n + 0 bpy.context.selected_objects
# def counted_reset(scene, depsgraph): )
# nonlocal molotov else:
# if molotov == 0: lock_depsgraph_update_n_times += len(
# reset() bpy.context.selected_objects
# else: )
# molotov -= 1
# # unregister previous resets to avoid multiple execution # NOTE: we reset with a timeout, as setting and resetting certain things
# if bpy.app.timers.is_registered(reset): # in fast succession will cause visual glitches (e.g. {}.data.use_path).
# bpy.app.timers.unregister(reset) def reset():
# if counted_reset in bpy.app.handlers.depsgraph_update_post: mom.data.use_path = previous_use_path
# bpy.app.handlers.depsgraph_update_post.remove(counted_reset) if counted_reset in bpy.app.handlers.depsgraph_update_post:
# if not isinstance(reset_timeout_s, bool): bpy.app.handlers.depsgraph_update_post.remove(counted_reset)
# if reset_timeout_s > 0: if bpy.app.timers.is_registered(reset):
# bpy.app.timers.register(reset, first_interval=reset_timeout_s) bpy.app.timers.unregister(reset)
# elif reset_timeout <= 0:
# reset() molotov = reset_depsgraph_n + 0
# bpy.app.handlers.depsgraph_update_post.append(counted_reset)
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() # endtime = time.perf_counter_ns()
# elapsedtime = endtime - starttime # elapsedtime = endtime - starttime
@ -1570,109 +1502,3 @@ def align_metrics_of_objects(objects=None):
add_metrics_obj_from_bound_box(t, bound_box) add_metrics_obj_from_bound_box(t, bound_box)
return "" 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 ""

View file

@ -1,4 +1,6 @@
from typing import TypedDict
from typing import Dict from typing import Dict
from dataclasses import dataclass
from pathlib import Path from pathlib import Path
# convenience dictionary for translating names to glyph ids # convenience dictionary for translating names to glyph ids
@ -43,9 +45,9 @@ known_misspellings = {
"overdot": "dotaccent", "overdot": "dotaccent",
"diaresis": "dieresis", "diaresis": "dieresis",
"diaeresis": "dieresis", "diaeresis": "dieresis",
# different conventions
"doubleacute": "hungarumlaut",
# character does not exist.. maybe something else # character does not exist.. maybe something else
"Odoubleacute": "Ohungarumlaut",
"Udoubleacute": "Uhungarumlaut",
"Wcaron": "Wcircumflex", "Wcaron": "Wcircumflex",
"Neng": "Nlongrightleg", "Neng": "Nlongrightleg",
"Lgrave": "Lacute", "Lgrave": "Lacute",
@ -75,13 +77,6 @@ def name_to_glyph(name):
return None 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): def is_space(character):
for name in space_d: for name in space_d:
if character == space_d[name][0]: if character == space_d[name][0]:

View file

@ -8,7 +8,7 @@ def get_version_minor():
def get_version_patch(): def get_version_patch():
return 6 return 5
def get_version_string(): def get_version_string():
@ -22,6 +22,7 @@ def prefix():
import datetime import datetime
import time import time
from mathutils import Vector
def get_timestamp(): def get_timestamp():