Compare commits
10 commits
Author | SHA1 | Date | |
---|---|---|---|
b40d49c723 | |||
490723496c | |||
2f94702ea9 | |||
f046546e61 | |||
167dea8164 | |||
36c8f25e29 | |||
d13afa7d7d | |||
1fbac99bd8 | |||
e69cdc951d | |||
cddbc79151 |
7 changed files with 166 additions and 35 deletions
|
@ -5,6 +5,7 @@
|
|||
/ ___ \| |_) | |___ ___) | |_| |
|
||||
/_/ \_\____/ \____|____/|____/
|
||||
```
|
||||
v0.0.4
|
||||
|
||||
Convenience tool to work with 3D typography in Blender and Cinema4D.
|
||||
|
||||
|
|
14
__init__.py
14
__init__.py
|
@ -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__':
|
||||
|
|
75
butils.py
75
butils.py
|
@ -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
|
||||
|
||||
|
|
|
@ -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
23
common/spacesUnicode.txt
Normal 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
|
|
@ -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
33
requirements.txt
Normal 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
|
Loading…
Reference in a new issue