Compare commits
No commits in common. "main" and "v0.0.3" have entirely different histories.
7 changed files with 35 additions and 166 deletions
|
@ -5,7 +5,6 @@
|
||||||
/ ___ \| |_) | |___ ___) | |_| |
|
/ ___ \| |_) | |___ ___) | |_| |
|
||||||
/_/ \_\____/ \____|____/|____/
|
/_/ \_\____/ \____|____/|____/
|
||||||
```
|
```
|
||||||
v0.0.4
|
|
||||||
|
|
||||||
Convenience tool to work with 3D typography in Blender and Cinema4D.
|
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 = {
|
bl_info = {
|
||||||
"name": "ABC3D",
|
"name": "ABC3D",
|
||||||
"author": "Jakob Schlötter, Studio Pointer*",
|
"author": "Jakob Schlötter, Studio Pointer*",
|
||||||
"version": (0, 0, 4),
|
"version": (0, 0, 3),
|
||||||
"blender": (4, 1, 0),
|
"blender": (4, 1, 0),
|
||||||
"location": "VIEW3D",
|
"location": "VIEW3D",
|
||||||
"description": "Convenience addon for 3D fonts",
|
"description": "Convenience addon for 3D fonts",
|
||||||
|
@ -242,6 +242,9 @@ class ABC3D_text_properties(bpy.types.PropertyGroup):
|
||||||
distribution_type: bpy.props.StringProperty()
|
distribution_type: bpy.props.StringProperty()
|
||||||
glyphs: bpy.props.CollectionProperty(type=ABC3D_glyph_properties)
|
glyphs: bpy.props.CollectionProperty(type=ABC3D_glyph_properties)
|
||||||
|
|
||||||
|
# TODO: simply, merge, cut cut cut
|
||||||
|
|
||||||
|
|
||||||
class ABC3D_data(bpy.types.PropertyGroup):
|
class ABC3D_data(bpy.types.PropertyGroup):
|
||||||
available_fonts: bpy.props.CollectionProperty(
|
available_fonts: bpy.props.CollectionProperty(
|
||||||
type=ABC3D_available_font, name="Available fonts")
|
type=ABC3D_available_font, name="Available fonts")
|
||||||
|
@ -1560,8 +1563,8 @@ def on_depsgraph_update(scene, depsgraph):
|
||||||
butils.run_in_main_thread(later)
|
butils.run_in_main_thread(later)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
print(f"REGISTER {utils.prefix()}")
|
|
||||||
addon_updater_ops.register(bl_info)
|
addon_updater_ops.register(bl_info)
|
||||||
|
|
||||||
for cls in classes:
|
for cls in classes:
|
||||||
|
@ -1569,8 +1572,9 @@ def register():
|
||||||
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.Object.__del__ = lambda self: print(f"Bye {self.name}")
|
# bpy.types.Object.__del__ = lambda self: print(f"Bye {self.name}")
|
||||||
|
print(f"REGISTER {bl_info['name']}")
|
||||||
|
|
||||||
# autostart if we load a blend file
|
# auto start if we load a blend file
|
||||||
if load_handler not in bpy.app.handlers.load_post:
|
if load_handler not in bpy.app.handlers.load_post:
|
||||||
bpy.app.handlers.load_post.append(load_handler)
|
bpy.app.handlers.load_post.append(load_handler)
|
||||||
# and autostart if we reload script
|
# and autostart if we reload script
|
||||||
|
@ -1589,7 +1593,7 @@ def register():
|
||||||
|
|
||||||
# bpy.ops.abc3d.load_installed_fonts()
|
# bpy.ops.abc3d.load_installed_fonts()
|
||||||
|
|
||||||
Font.init()
|
Font.name_to_glyph_d = Font.generate_name_to_glyph_d()
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
|
@ -1611,7 +1615,7 @@ 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
|
||||||
print(f"UNREGISTER {utils.prefix()}")
|
print(f"UNREGISTER {bl_info['name']}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
73
butils.py
73
butils.py
|
@ -553,9 +553,7 @@ def register_installed_fonts():
|
||||||
# print(f"available font: {f.font_name} {f.face_name}")
|
# print(f"available font: {f.font_name} {f.face_name}")
|
||||||
register_font_from_filepath(font_path)
|
register_font_from_filepath(font_path)
|
||||||
|
|
||||||
message_memory = []
|
def ShowMessageBox(title = "Message Box", icon = 'INFO', message=""):
|
||||||
|
|
||||||
def ShowMessageBox(title = "Message Box", icon = 'INFO', message="", prevent_repeat=False):
|
|
||||||
|
|
||||||
"""Show a simple message box
|
"""Show a simple message box
|
||||||
|
|
||||||
|
@ -581,13 +579,6 @@ def ShowMessageBox(title = "Message Box", icon = 'INFO', message="", prevent_rep
|
||||||
butils.ShowMessageBox(title="",message=("AAAAAH","NOOOOO"),icon=)
|
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
|
myLines=message
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
if isinstance(myLines, str):
|
if isinstance(myLines, str):
|
||||||
|
@ -674,21 +665,11 @@ def get_glyph_height(glyph_obj):
|
||||||
return abs(c.bound_box[0][1] - c.bound_box[3][1])
|
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])
|
return abs(glyph_obj.bound_box[0][1] - glyph_obj.bound_box[3][1])
|
||||||
|
|
||||||
def prepare_text(font_name, face_name, text, allow_replacement=True):
|
def prepare_text(font_name, face_name, text):
|
||||||
loaded, missing, loadable, files = Font.test_glyphs_availability(
|
loaded, missing, loadable, files = Font.test_glyphs_availability(
|
||||||
font_name,
|
font_name,
|
||||||
face_name,
|
face_name,
|
||||||
text)
|
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:
|
if len(loadable) > 0:
|
||||||
for filepath in files:
|
for filepath in files:
|
||||||
load_font_from_filepath(filepath, loadable, font_name, face_name)
|
load_font_from_filepath(filepath, loadable, font_name, face_name)
|
||||||
|
@ -780,6 +761,9 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4)
|
||||||
if c == '\\':
|
if c == '\\':
|
||||||
is_command = True
|
is_command = True
|
||||||
continue
|
continue
|
||||||
|
if c == ' ':
|
||||||
|
advance = advance + scalor
|
||||||
|
continue
|
||||||
is_newline = False
|
is_newline = False
|
||||||
if is_command:
|
if is_command:
|
||||||
if c == 'n':
|
if c == 'n':
|
||||||
|
@ -794,33 +778,14 @@ 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
|
||||||
|
|
||||||
glyph_tmp = Font.get_glyph(text_properties.font_name,
|
glyph = Font.get_glyph(text_properties.font_name,
|
||||||
text_properties.face_name,
|
text_properties.face_name,
|
||||||
glyph_id)
|
glyph_id).original
|
||||||
if glyph_tmp == None:
|
|
||||||
space_width = Font.is_space(glyph_id)
|
|
||||||
if space_width != False:
|
|
||||||
advance = advance + space_width * text_properties.font_size
|
|
||||||
continue
|
|
||||||
|
|
||||||
message=f"Glyph not found for font_name='{text_properties.font_name}' face_name='{text_properties.face_name}' glyph_id='{glyph_id}'"
|
if glyph == None:
|
||||||
replaced = False
|
# self.report({'ERROR'}, f"Glyph not found for {font_name} {face_name} {glyph_id}")
|
||||||
if glyph_id.isalpha():
|
print(f"Glyph not found for {text_properties.font_name} {text_properties.face_name} {glyph_id}")
|
||||||
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
|
continue
|
||||||
glyph = glyph_tmp.original
|
|
||||||
|
|
||||||
ob = None
|
ob = None
|
||||||
obg = None
|
obg = None
|
||||||
|
@ -847,15 +812,6 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4)
|
||||||
ob.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_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)
|
location, tangent, spline_index = calc_point_on_bezier_curve(mom, advance, True, True)
|
||||||
if spline_index != previous_spline_index:
|
if spline_index != previous_spline_index:
|
||||||
is_newline = True
|
is_newline = True
|
||||||
|
@ -881,7 +837,10 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4)
|
||||||
vectors,
|
vectors,
|
||||||
factors,
|
factors,
|
||||||
local_main_axis)
|
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 = mathutils.Quaternion()
|
||||||
q.rotate(text_properties.orientation)
|
q.rotate(text_properties.orientation)
|
||||||
if regenerate:
|
if regenerate:
|
||||||
|
@ -896,10 +855,6 @@ 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()
|
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()
|
# 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
|
glyph_advance = get_glyph_advance(glyph) * scalor + text_properties.letter_spacing
|
||||||
|
|
||||||
|
|
|
@ -37,8 +37,6 @@ name_to_glyph_d = {
|
||||||
"space": " ",
|
"space": " ",
|
||||||
}
|
}
|
||||||
|
|
||||||
space_d = {}
|
|
||||||
|
|
||||||
known_misspellings = {
|
known_misspellings = {
|
||||||
# simple misspelling
|
# simple misspelling
|
||||||
"excent" : "accent",
|
"excent" : "accent",
|
||||||
|
@ -76,44 +74,17 @@ def name_to_glyph(name):
|
||||||
else:
|
else:
|
||||||
return None
|
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 = {}
|
d = {}
|
||||||
with open(filepath) as f:
|
with open(f"{Path(__file__).parent}/glyphNamesToUnicode.txt") as f:
|
||||||
for line in f:
|
for line in f:
|
||||||
if line[0] == '#':
|
if line[0] == '#':
|
||||||
continue
|
continue
|
||||||
split = line.split(' ')
|
|
||||||
if len(split) == 2:
|
|
||||||
(name, hexstr) = line.split(' ')
|
(name, hexstr) = line.split(' ')
|
||||||
val = chr(int(hexstr, base=16))
|
val = chr(int(hexstr, base=16))
|
||||||
d[name] = val
|
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
|
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:
|
class FontFace:
|
||||||
"""FontFace is a class holding glyphs
|
"""FontFace is a class holding glyphs
|
||||||
|
@ -203,19 +174,19 @@ def get_glyph(font_name, face_name, glyph_id, alternate=0):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not fonts.keys().__contains__(font_name):
|
if not fonts.keys().__contains__(font_name):
|
||||||
# print(f"ABC3D::get_glyph: font name({font_name}) not found")
|
print(f"ABC3D::get_glyph: font name({font_name}) not found")
|
||||||
# print(fonts.keys())
|
print(fonts.keys())
|
||||||
return None
|
return None
|
||||||
|
|
||||||
face = fonts[font_name].faces.get(face_name)
|
face = fonts[font_name].faces.get(face_name)
|
||||||
if face == None:
|
if face == None:
|
||||||
# print(f"ABC3D::get_glyph: font({font_name}) face({face_name}) not found")
|
print(f"ABC3D::get_glyph: font({font_name}) face({face_name}) not found")
|
||||||
# print(fonts[font_name].faces.keys())
|
print(fonts[font_name].faces.keys())
|
||||||
return None
|
return None
|
||||||
|
|
||||||
glyphs_for_id = face.glyphs.get(glyph_id)
|
glyphs_for_id = face.glyphs.get(glyph_id)
|
||||||
if glyphs_for_id == None or len(glyphs_for_id) <= alternate:
|
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:
|
if glyph_id not in fonts[font_name].faces[face_name].missing_glyphs:
|
||||||
fonts[font_name].faces[face_name].missing_glyphs.append(glyph_id)
|
fonts[font_name].faces[face_name].missing_glyphs.append(glyph_id)
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
# 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():
|
def get_version_minor():
|
||||||
return 0
|
return 0
|
||||||
def get_version_patch():
|
def get_version_patch():
|
||||||
return 4
|
return 3
|
||||||
def get_version_string():
|
def get_version_string():
|
||||||
return f"{get_version_major()}.{get_version_minor()}.{get_version_patch}"
|
return f"{get_version_major()}.{get_version_minor()}.{get_version_patch}"
|
||||||
def prefix():
|
def prefix():
|
||||||
|
@ -72,10 +72,6 @@ def printerr(*args, **kwargs):
|
||||||
print(*args, file=sys.stderr, **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
|
# # Evaluate a bezier curve for the parameter 0<=t<=1 along its length
|
||||||
# def evaluateBezierPoint(p1, h1, h2, p2, t):
|
# 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
|
# return ((1 - t)**3) * p1 + (3 * t * (1 - t)**2) * h1 + (3 * (t**2) * (1 - t)) * h2 + (t**3) * p2
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
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