Compare commits
7 commits
777644e509
...
19f4bf586f
Author | SHA1 | Date | |
---|---|---|---|
19f4bf586f | |||
04229fbc31 | |||
963d89daf9 | |||
3ef2ae934d | |||
8965ab11eb | |||
513497d492 | |||
335ab1face |
5 changed files with 376 additions and 161 deletions
|
@ -5,7 +5,7 @@
|
|||
/ ___ \| |_) | |___ ___) | |_| |
|
||||
/_/ \_\____/ \____|____/|____/
|
||||
```
|
||||
v0.0.9
|
||||
v0.0.10
|
||||
|
||||
Convenience tool to work with 3D typography in Blender and Cinema4D.
|
||||
|
||||
|
|
222
__init__.py
222
__init__.py
|
@ -16,7 +16,7 @@ from .common import Font, utils
|
|||
bl_info = {
|
||||
"name": "ABC3D",
|
||||
"author": "Jakob Schlötter, Studio Pointer*",
|
||||
"version": (0, 0, 9),
|
||||
"version": (0, 0, 10),
|
||||
"blender": (4, 1, 0),
|
||||
"location": "VIEW3D",
|
||||
"description": "Convenience addon for 3D fonts",
|
||||
|
@ -35,7 +35,6 @@ if "Font" in locals():
|
|||
importlib.reload(bimport)
|
||||
importlib.reload(addon_updater_ops)
|
||||
|
||||
|
||||
def getPreferences(context):
|
||||
preferences = context.preferences
|
||||
return preferences.addons[__name__].preferences
|
||||
|
@ -398,51 +397,51 @@ class ABC3D_PT_FontList(bpy.types.Panel):
|
|||
available_font = abc3d_data.available_fonts[abc3d_data.active_font_index]
|
||||
font_name = available_font.font_name
|
||||
face_name = available_font.face_name
|
||||
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
|
||||
]
|
||||
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"
|
||||
)
|
||||
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
|
||||
oper_lf.font_name = font_name
|
||||
oper_lf.face_name = face_name
|
||||
|
||||
|
||||
class ABC3D_PT_TextPlacement(bpy.types.Panel):
|
||||
|
@ -754,7 +753,6 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel):
|
|||
return t
|
||||
return None
|
||||
|
||||
# NOTE: HERE
|
||||
def get_active_glyph_properties(self):
|
||||
a_o = bpy.context.active_object
|
||||
if a_o is not None:
|
||||
|
@ -1026,7 +1024,11 @@ class ABC3D_OT_LoadFont(bpy.types.Operator):
|
|||
face_name: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
filepaths = Font.fonts[self.font_name].faces[self.face_name].filepaths
|
||||
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
|
||||
for f in filepaths:
|
||||
butils.load_font_from_filepath(f)
|
||||
return {"FINISHED"}
|
||||
|
@ -1402,6 +1404,9 @@ 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)
|
||||
|
@ -1425,30 +1430,95 @@ 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
|
||||
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
|
||||
]
|
||||
)
|
||||
scale_y = 0.5
|
||||
row = subbox.row()
|
||||
row.scale_y = scale_y
|
||||
row.label(text=text)
|
||||
layout.prop(abc3d_data, "export_dir")
|
||||
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=}")
|
||||
print(f"{utils.prefix()} {Font.fonts=}")
|
||||
|
||||
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")
|
||||
|
||||
|
@ -1528,7 +1598,10 @@ class ABC3D_OT_SaveFontToFile(bpy.types.Operator):
|
|||
use_selection=True,
|
||||
use_active_scene=True,
|
||||
)
|
||||
bpy.app.timers.register(lambda: bpy.ops.scene.delete(), first_interval=1)
|
||||
def delete_scene():
|
||||
bpy.ops.scene.delete()
|
||||
return None
|
||||
bpy.app.timers.register(lambda: delete_scene(), first_interval=1)
|
||||
|
||||
# bpy.ops.scene.delete()
|
||||
# restore()
|
||||
|
@ -1541,7 +1614,7 @@ class ABC3D_OT_SaveFontToFile(bpy.types.Operator):
|
|||
return None
|
||||
|
||||
bpy.app.timers.register(lambda: remove_faces(), first_interval=2)
|
||||
self.report({"INFO"}, "did it")
|
||||
self.report({"INFO"}, f"{utils.prefix()}::save_font_to_file done")
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
@ -1669,9 +1742,6 @@ 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:
|
||||
|
@ -1842,7 +1912,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 type(a) == type(int()):
|
||||
if isinstance(a, int):
|
||||
if a == Font.MISSING_FONT:
|
||||
butils.ShowMessageBox(
|
||||
"Missing Font",
|
||||
|
@ -1859,9 +1929,9 @@ def load_used_glyphs():
|
|||
"Do you have it installed?",
|
||||
],
|
||||
)
|
||||
elif len(a["maybe"]) > 0:
|
||||
for fp in a["filepaths"]:
|
||||
butils.load_font_from_filepath(fp, a["maybe"])
|
||||
elif len(a.unloaded) > 0:
|
||||
for fp in a.filepaths:
|
||||
butils.load_font_from_filepath(fp, a.unloaded)
|
||||
|
||||
|
||||
@persistent
|
||||
|
|
179
butils.py
179
butils.py
|
@ -94,6 +94,8 @@ 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
|
||||
|
@ -126,6 +128,8 @@ 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
|
||||
|
@ -134,6 +138,24 @@ 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(
|
||||
|
@ -193,6 +215,34 @@ def calc_bezier_length(bezier_point_1, bezier_point_2, resolution=20):
|
|||
return length
|
||||
|
||||
|
||||
def get_real_beziers_and_lengths(bezier_spline_obj, resolution_factor):
|
||||
beziers = []
|
||||
lengths = []
|
||||
total_length = 0
|
||||
n_bezier_points = len(bezier_spline_obj.bezier_points)
|
||||
real_n_bezier_points = len(bezier_spline_obj.bezier_points)
|
||||
if bezier_spline_obj.use_cyclic_u:
|
||||
n_bezier_points += 1
|
||||
for i in range(0, n_bezier_points - 1):
|
||||
i_a = i % (n_bezier_points - 1)
|
||||
i_b = (i_a + 1) % real_n_bezier_points
|
||||
bezier = [
|
||||
bezier_spline_obj.bezier_points[i_a],
|
||||
bezier_spline_obj.bezier_points[i_b],
|
||||
]
|
||||
length = calc_bezier_length(
|
||||
bezier[0],
|
||||
bezier[1],
|
||||
int(bezier_spline_obj.resolution_u * resolution_factor),
|
||||
)
|
||||
total_length += length
|
||||
beziers.append(bezier)
|
||||
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
|
||||
):
|
||||
|
@ -219,6 +269,17 @@ def calc_point_on_bezier_spline(
|
|||
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)
|
||||
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]
|
||||
|
@ -227,30 +288,8 @@ def calc_point_on_bezier_spline(
|
|||
else:
|
||||
return location
|
||||
|
||||
beziers = []
|
||||
lengths = []
|
||||
total_length = 0
|
||||
n_bezier_points = len(bezier_spline_obj.bezier_points)
|
||||
real_n_bezier_points = len(bezier_spline_obj.bezier_points)
|
||||
if bezier_spline_obj.use_cyclic_u:
|
||||
n_bezier_points += 1
|
||||
for i in range(0, n_bezier_points - 1):
|
||||
i_a = i % (n_bezier_points - 1)
|
||||
i_b = (i_a + 1) % real_n_bezier_points
|
||||
bezier = [
|
||||
bezier_spline_obj.bezier_points[i_a],
|
||||
bezier_spline_obj.bezier_points[i_b],
|
||||
]
|
||||
length = calc_bezier_length(
|
||||
bezier[0],
|
||||
bezier[1],
|
||||
int(bezier_spline_obj.resolution_u * resolution_factor),
|
||||
)
|
||||
total_length += length
|
||||
beziers.append(bezier)
|
||||
lengths.append(length)
|
||||
# if total_length > distance:
|
||||
# break
|
||||
beziers, lengths, total_length = get_real_beziers_and_lengths(bezier_spline_obj,
|
||||
resolution_factor)
|
||||
|
||||
iterated_distance = 0
|
||||
for i in range(0, len(beziers)):
|
||||
|
@ -268,7 +307,16 @@ 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)
|
||||
|
@ -515,7 +563,11 @@ def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""):
|
|||
|
||||
for mff in modified_font_faces:
|
||||
mff_glyphs = []
|
||||
face = Font.fonts[mff["font_name"]].faces[mff["face_name"]]
|
||||
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"modified font face {mff=} could not be accessed.")
|
||||
continue
|
||||
# iterate glyphs
|
||||
for g in face.glyphs:
|
||||
# iterate alternates
|
||||
|
@ -543,17 +595,18 @@ def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""):
|
|||
def update_available_fonts():
|
||||
abc3d_data = bpy.context.scene.abc3d_data
|
||||
|
||||
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}")
|
||||
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}")
|
||||
|
||||
|
||||
# def update_available_texts():
|
||||
|
@ -754,21 +807,28 @@ def get_glyph_height(glyph_obj):
|
|||
|
||||
|
||||
def prepare_text(font_name, face_name, text, allow_replacement=True):
|
||||
loaded, missing, loadable, files = Font.test_glyphs_availability(
|
||||
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
|
||||
# possibly replace upper and lower case letters with each other
|
||||
if len(missing) > 0 and allow_replacement:
|
||||
if len(availability.missing) > 0 and allow_replacement:
|
||||
replacement_search = ""
|
||||
for m in missing:
|
||||
for m in availability.missing:
|
||||
if m.isalpha():
|
||||
replacement_search += m.swapcase()
|
||||
r = Font.test_availability(font_name, face_name, replacement_search)
|
||||
loadable += r["maybe"]
|
||||
loadable += r.unloaded
|
||||
# not update (loaded, missing, files), we only use loadable/maybe later
|
||||
|
||||
if len(loadable) > 0:
|
||||
for filepath in files:
|
||||
for filepath in availability.filepaths:
|
||||
load_font_from_filepath(filepath, loadable, font_name, face_name)
|
||||
return True
|
||||
|
||||
|
@ -776,9 +836,9 @@ 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"]:
|
||||
for c in availability.missing:
|
||||
t_text = t_text.replace(c, "")
|
||||
for c in AVAILABILITY["missing"]:
|
||||
for c in AVAILABILITY.missing:
|
||||
t_text = t_text.replace(c, "")
|
||||
return t_text
|
||||
|
||||
|
@ -1016,14 +1076,8 @@ def transfer_text_object_to_text_properties(text_object, text_properties, id_fro
|
|||
if text == text_properties.text:
|
||||
is_good_text = True
|
||||
else:
|
||||
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"]:
|
||||
t_text = t_text.replace(c, "")
|
||||
for c in AVAILABILITY["missing"]:
|
||||
t_text = t_text.replace(c, "")
|
||||
if len(t_text) == len(text):
|
||||
t_text = predict_actual_text(text_properties)
|
||||
if t_text == text:
|
||||
is_good_text = True
|
||||
if is_good_text:
|
||||
text_properties.actual_text = text
|
||||
|
@ -1252,7 +1306,7 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4,
|
|||
|
||||
actual_text = ""
|
||||
for i, c in enumerate(text_properties.text):
|
||||
face = Font.fonts[text_properties.font_name].faces[text_properties.face_name]
|
||||
face = Font.get_font_face(text_properties.font_name, text_properties.face_name)
|
||||
scalor = face.unit_factor * text_properties.font_size
|
||||
if c == "\\":
|
||||
is_command = True
|
||||
|
@ -1446,26 +1500,39 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4,
|
|||
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:
|
||||
).length > glyph_advance and psi == si and n < n_max:
|
||||
curve_compensation = curve_compensation - glyph_advance * 0.01
|
||||
new_location, si = calc_point_on_bezier_curve(
|
||||
tmp_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:
|
||||
).length < glyph_advance and psi == si and n < n_max:
|
||||
curve_compensation = curve_compensation + glyph_advance * 0.01
|
||||
new_location, si = calc_point_on_bezier_curve(
|
||||
tmp_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
|
||||
|
|
108
common/Font.py
108
common/Font.py
|
@ -1,5 +1,6 @@
|
|||
from typing import Dict
|
||||
from pathlib import Path
|
||||
from typing import NamedTuple
|
||||
|
||||
# convenience dictionary for translating names to glyph ids
|
||||
# note: overwritten/extended by the content of "glypNamesToUnicode.txt"
|
||||
|
@ -160,6 +161,7 @@ class Font:
|
|||
self.faces = faces
|
||||
|
||||
|
||||
|
||||
def register_font(font_name, face_name, glyphs_in_fontfile, filepath):
|
||||
if not fonts.keys().__contains__(font_name):
|
||||
fonts[font_name] = Font({})
|
||||
|
@ -177,6 +179,34 @@ 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
|
||||
|
@ -203,6 +233,38 @@ 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")
|
||||
print(fonts[font_name].faces.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, 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
|
||||
|
@ -218,25 +280,22 @@ def get_glyph(font_name, face_name, glyph_id, alternate=0):
|
|||
:rtype: `Object`
|
||||
"""
|
||||
|
||||
if not fonts.keys().__contains__(font_name):
|
||||
# print(f"ABC3D::get_glyph: font name({font_name}) not found")
|
||||
# print(fonts.keys())
|
||||
glyphs = get_glyphs(font_name, face_name, glyph_id)
|
||||
|
||||
if len(glyphs) <= alternate or len(glyphs) == 0:
|
||||
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 is 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 is 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
|
||||
|
||||
return fonts[font_name].faces[face_name].glyphs.get(glyph_id)[alternate]
|
||||
class GlyphsAvailability(NamedTuple):
|
||||
loaded: str
|
||||
missing: str
|
||||
unloaded: str
|
||||
filepaths: list[str]
|
||||
|
||||
|
||||
def test_glyphs_availability(font_name, face_name, text):
|
||||
|
@ -245,24 +304,24 @@ def test_glyphs_availability(font_name, face_name, text):
|
|||
not fonts.keys().__contains__(font_name)
|
||||
or fonts[font_name].faces.get(face_name) is None
|
||||
):
|
||||
return "", "", text # <loaded>, <missing>, <maybe>
|
||||
return GlyphsAvailability("", "", "", [])
|
||||
|
||||
loaded = []
|
||||
missing = []
|
||||
maybe = []
|
||||
unloaded = []
|
||||
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:
|
||||
maybe.append(c)
|
||||
unloaded.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 (
|
||||
return GlyphsAvailability(
|
||||
"".join(loaded),
|
||||
"".join(missing),
|
||||
"".join(maybe),
|
||||
"".join(unloaded),
|
||||
fonts[font_name].faces[face_name].filepaths,
|
||||
)
|
||||
|
||||
|
@ -288,15 +347,10 @@ def test_availability(font_name, face_name, text):
|
|||
return MISSING_FONT
|
||||
if fonts[font_name].faces.get(face_name) is None:
|
||||
return MISSING_FACE
|
||||
loaded, missing, maybe, filepaths = test_glyphs_availability(
|
||||
availability: GlyphsAvailability = test_glyphs_availability(
|
||||
font_name, face_name, text
|
||||
)
|
||||
return {
|
||||
"loaded": loaded,
|
||||
"missing": missing,
|
||||
"maybe": maybe,
|
||||
"filepaths": filepaths,
|
||||
}
|
||||
return availability
|
||||
|
||||
|
||||
# holds all fonts
|
||||
|
|
|
@ -8,7 +8,7 @@ def get_version_minor():
|
|||
|
||||
|
||||
def get_version_patch():
|
||||
return 9
|
||||
return 10
|
||||
|
||||
|
||||
def get_version_string():
|
||||
|
@ -82,6 +82,10 @@ 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)
|
||||
|
||||
|
@ -89,6 +93,26 @@ def printerr(*args, **kwargs):
|
|||
def removeNonAlphabetic(s):
|
||||
return "".join([i for i in s if i.isalpha()])
|
||||
|
||||
import pathlib
|
||||
import os
|
||||
|
||||
def can_create_path(path_str : str):
|
||||
path = pathlib.Path(path_str).absolute().resolve()
|
||||
|
||||
while True: # this looks dangerours, but it actually is not
|
||||
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
|
||||
|
||||
|
||||
|
||||
# # Evaluate a bezier curve for the parameter 0<=t<=1 along its length
|
||||
# def evaluateBezierPoint(p1, h1, h2, p2, t):
|
||||
|
|
Loading…
Add table
Reference in a new issue