Compare commits

...

10 commits
v0.0.3 ... main

Author SHA1 Message Date
b40d49c723 bump version v0.0.4 2025-01-19 14:24:23 +01:00
490723496c simplify 2025-01-19 14:21:21 +01:00
2f94702ea9 add removeNonAlphabetic 2025-01-19 14:20:37 +01:00
f046546e61 less print 2025-01-19 14:20:14 +01:00
167dea8164 allow replacements (upper/lower) 2025-01-19 14:19:59 +01:00
36c8f25e29 reset rotation mode 2025-01-19 12:03:42 +01:00
d13afa7d7d friendliness
prevent repetitive messages
2025-01-18 18:22:32 +01:00
1fbac99bd8 add space recognition 2025-01-18 18:21:33 +01:00
e69cdc951d cosmetics 2025-01-18 18:19:52 +01:00
cddbc79151 add requirements.txt
useful for development
2025-01-18 17:18:23 +01:00
7 changed files with 166 additions and 35 deletions

View file

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

View file

@ -15,7 +15,7 @@ import importlib
bl_info = {
"name": "ABC3D",
"author": "Jakob Schlötter, Studio Pointer*",
"version": (0, 0, 3),
"version": (0, 0, 4),
"blender": (4, 1, 0),
"location": "VIEW3D",
"description": "Convenience addon for 3D fonts",
@ -242,9 +242,6 @@ class ABC3D_text_properties(bpy.types.PropertyGroup):
distribution_type: bpy.props.StringProperty()
glyphs: bpy.props.CollectionProperty(type=ABC3D_glyph_properties)
# TODO: simply, merge, cut cut cut
class ABC3D_data(bpy.types.PropertyGroup):
available_fonts: bpy.props.CollectionProperty(
type=ABC3D_available_font, name="Available fonts")
@ -1563,8 +1560,8 @@ def on_depsgraph_update(scene, depsgraph):
butils.run_in_main_thread(later)
def register():
print(f"REGISTER {utils.prefix()}")
addon_updater_ops.register(bl_info)
for cls in classes:
@ -1572,9 +1569,8 @@ def register():
bpy.utils.register_class(cls)
bpy.types.Scene.abc3d_data = bpy.props.PointerProperty(type=ABC3D_data)
# bpy.types.Object.__del__ = lambda self: print(f"Bye {self.name}")
print(f"REGISTER {bl_info['name']}")
# auto start if we load a blend file
# autostart if we load a blend file
if load_handler not in bpy.app.handlers.load_post:
bpy.app.handlers.load_post.append(load_handler)
# and autostart if we reload script
@ -1593,7 +1589,7 @@ def register():
# bpy.ops.abc3d.load_installed_fonts()
Font.name_to_glyph_d = Font.generate_name_to_glyph_d()
Font.init()
def unregister():
@ -1615,7 +1611,7 @@ def unregister():
bpy.app.handlers.depsgraph_update_post.remove(on_depsgraph_update)
del bpy.types.Scene.abc3d_data
print(f"UNREGISTER {bl_info['name']}")
print(f"UNREGISTER {utils.prefix()}")
if __name__ == '__main__':

View file

@ -553,7 +553,9 @@ def register_installed_fonts():
# print(f"available font: {f.font_name} {f.face_name}")
register_font_from_filepath(font_path)
def ShowMessageBox(title = "Message Box", icon = 'INFO', message=""):
message_memory = []
def ShowMessageBox(title = "Message Box", icon = 'INFO', message="", prevent_repeat=False):
"""Show a simple message box
@ -579,6 +581,13 @@ def ShowMessageBox(title = "Message Box", icon = 'INFO', message=""):
butils.ShowMessageBox(title="",message=("AAAAAH","NOOOOO"),icon=)
"""
global message_memory
if prevent_repeat:
for m in message_memory:
if m[0] == title and m[1] == icon and m[2] == message:
print("PREVENT PREVENT")
return
message_memory.append([title, icon, message])
myLines=message
def draw(self, context):
if isinstance(myLines, str):
@ -665,11 +674,21 @@ def get_glyph_height(glyph_obj):
return abs(c.bound_box[0][1] - c.bound_box[3][1])
return abs(glyph_obj.bound_box[0][1] - glyph_obj.bound_box[3][1])
def prepare_text(font_name, face_name, text):
def prepare_text(font_name, face_name, text, allow_replacement=True):
loaded, missing, loadable, files = Font.test_glyphs_availability(
font_name,
face_name,
text)
# possibly replace upper and lower case letters with each other
if len(missing) > 0 and allow_replacement:
replacement_search = ""
for m in missing:
if m.isalpha():
replacement_search += m.swapcase()
r = Font.test_availability(font_name, face_name, replacement_search)
loadable += r["maybe"]
# not update (loaded, missing, files), we only use loadable/maybe later
if len(loadable) > 0:
for filepath in files:
load_font_from_filepath(filepath, loadable, font_name, face_name)
@ -761,9 +780,6 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4)
if c == '\\':
is_command = True
continue
if c == ' ':
advance = advance + scalor
continue
is_newline = False
if is_command:
if c == 'n':
@ -778,14 +794,33 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4)
is_command = False
glyph_id = c
glyph = Font.get_glyph(text_properties.font_name,
glyph_tmp = Font.get_glyph(text_properties.font_name,
text_properties.face_name,
glyph_id).original
glyph_id)
if glyph_tmp == None:
space_width = Font.is_space(glyph_id)
if space_width != False:
advance = advance + space_width * text_properties.font_size
continue
if glyph == None:
# self.report({'ERROR'}, f"Glyph not found for {font_name} {face_name} {glyph_id}")
print(f"Glyph not found for {text_properties.font_name} {text_properties.face_name} {glyph_id}")
continue
message=f"Glyph not found for font_name='{text_properties.font_name}' face_name='{text_properties.face_name}' glyph_id='{glyph_id}'"
replaced = False
if glyph_id.isalpha():
possible_replacement = glyph_id.swapcase()
glyph_tmp = Font.get_glyph(text_properties.font_name,
text_properties.face_name,
possible_replacement)
if glyph_tmp != None:
message = message + f" (replaced with '{possible_replacement}')"
replaced = True
ShowMessageBox(title="Glyph replaced" if replaced else "Glyph missing",
icon='INFO' if replaced else 'ERROR',
message=message,
prevent_repeat=True)
if replaced == False:
continue
glyph = glyph_tmp.original
ob = None
obg = None
@ -812,6 +847,15 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4)
ob.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
location, tangent, spline_index = calc_point_on_bezier_curve(mom, advance, True, True)
if spline_index != previous_spline_index:
is_newline = True
@ -837,10 +881,7 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4)
vectors,
factors,
local_main_axis)
if ob.rotation_mode != 'QUATERNION':
ob.rotation_mode = 'QUATERNION'
if obg.rotation_mode != 'QUATERNION':
obg.rotation_mode = 'QUATERNION'
q = mathutils.Quaternion()
q.rotate(text_properties.orientation)
if regenerate:
@ -855,6 +896,10 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4)
obg.rotation_quaternion = (mom.matrix_world @ q.to_matrix().to_4x4()).to_quaternion()
# ob.rotation_quaternion = (mom.matrix_world @ q.to_matrix().to_4x4()).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
glyph_advance = get_glyph_advance(glyph) * scalor + text_properties.letter_spacing

View file

@ -37,6 +37,8 @@ name_to_glyph_d = {
"space": " ",
}
space_d = {}
known_misspellings = {
# simple misspelling
"excent" : "accent",
@ -74,17 +76,44 @@ def name_to_glyph(name):
else:
return None
def generate_name_to_glyph_d():
def is_space(character):
for name in space_d:
if character == space_d[name][0]:
return space_d[name][1]
return False
def generate_from_file_d(filepath):
d = {}
with open(f"{Path(__file__).parent}/glyphNamesToUnicode.txt") as f:
with open(filepath) as f:
for line in f:
if line[0] == '#':
continue
(name, hexstr) = line.split(' ')
val = chr(int(hexstr, base=16))
d[name] = val
split = line.split(' ')
if len(split) == 2:
(name, hexstr) = line.split(' ')
val = chr(int(hexstr, base=16))
d[name] = val
if len(split) == 3:
# we might have a parameter, like for the spaces
(name, hexstr, parameter) = line.split(' ')
parameter_value = float(parameter)
val = chr(int(hexstr, base=16))
d[name] = [val, parameter_value]
return d
def generate_name_to_glyph_d():
return generate_from_file_d(f"{Path(__file__).parent}/glyphNamesToUnicode.txt")
def generate_space_d():
return generate_from_file_d(f"{Path(__file__).parent}/spacesUnicode.txt")
def init():
global name_to_glyph_d
global space_d
name_to_glyph_d = generate_name_to_glyph_d()
space_d = generate_space_d()
class FontFace:
"""FontFace is a class holding glyphs
@ -174,19 +203,19 @@ def get_glyph(font_name, face_name, glyph_id, alternate=0):
"""
if not fonts.keys().__contains__(font_name):
print(f"ABC3D::get_glyph: font name({font_name}) not found")
print(fonts.keys())
# print(f"ABC3D::get_glyph: font name({font_name}) not found")
# print(fonts.keys())
return None
face = fonts[font_name].faces.get(face_name)
if face == None:
print(f"ABC3D::get_glyph: font({font_name}) face({face_name}) not found")
print(fonts[font_name].faces.keys())
# print(f"ABC3D::get_glyph: font({font_name}) face({face_name}) not found")
# print(fonts[font_name].faces.keys())
return None
glyphs_for_id = face.glyphs.get(glyph_id)
if glyphs_for_id == None or len(glyphs_for_id) <= alternate:
print(f"ABC3D::get_glyph: font({font_name}) face({face_name}) glyph({glyph_id})[{alternate}] not found")
# 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

23
common/spacesUnicode.txt Normal file
View file

@ -0,0 +1,23 @@
# The space value derives from The Elements of Typographic Style
# same for en-/em values. Rest are rough guesses.
space 0020 0.25
nbspace 00A0 0.25
# ethi:wordspace 1361 # NOTE: has shape
enquad 2000 0.5
emquad 2001 1
enspace 2002 0.5
emspace 2003 1
threeperemspace 2004 3
fourperemspace 2005 4
sixperemspace 2006 6
figurespace 2007 1
punctuationspace 2008 1
thinspace 2009 0.1
hairspace 200A 0.05
zerowidthspace 200B 0
narrownobreakspace 202F 0.1
mediummathematicalspace 205F 1
cntr:space 2420 0.25
ideographicspace 3000 1
# ideographichalffillspace 303F # NOTE: has shape
zerowidthnobreakspace FEFF 0

View file

@ -4,7 +4,7 @@ def get_version_major():
def get_version_minor():
return 0
def get_version_patch():
return 3
return 4
def get_version_string():
return f"{get_version_major()}.{get_version_minor()}.{get_version_patch}"
def prefix():
@ -72,6 +72,10 @@ def printerr(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)
def removeNonAlphabetic(s):
return ''.join([i for i in s if i.isalpha()])
# # Evaluate a bezier curve for the parameter 0<=t<=1 along its length
# def evaluateBezierPoint(p1, h1, h2, p2, t):
# return ((1 - t)**3) * p1 + (3 * t * (1 - t)**2) * h1 + (3 * (t**2) * (1 - t)) * h2 + (t**3) * p2

33
requirements.txt Normal file
View file

@ -0,0 +1,33 @@
astroid==3.3.5
attrs==24.2.0
black==24.10.0
bpy==4.2.0
cattrs==24.1.2
certifi==2024.8.30
charset-normalizer==3.4.0
click==8.1.7
Cython==3.0.11
dill==0.3.9
docstring-to-markdown==0.15
flake8==7.1.1
idna==3.10
isort==5.13.2
jedi==0.19.1
jedi-language-server==0.41.4
lsprotocol==2023.0.1
mathutils==3.3.0
mccabe==0.7.0
mypy-extensions==1.0.0
numpy==2.1.3
packaging==24.1
parso==0.8.4
pathspec==0.12.1
platformdirs==4.3.6
pycodestyle==2.12.1
pyflakes==3.2.0
pygls==1.3.1
pylint==3.3.1
requests==2.32.3
tomlkit==0.13.2
urllib3==2.2.3
zstandard==0.23.0