Merge branch 'main' into dev
This commit is contained in:
commit
5bd78a3fc1
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.
|
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, 3),
|
"version": (0, 0, 4),
|
||||||
"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,9 +242,6 @@ 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")
|
||||||
|
@ -1563,8 +1560,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:
|
||||||
|
@ -1572,9 +1569,8 @@ 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']}")
|
|
||||||
|
|
||||||
# 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:
|
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
|
||||||
|
@ -1593,7 +1589,7 @@ def register():
|
||||||
|
|
||||||
# bpy.ops.abc3d.load_installed_fonts()
|
# bpy.ops.abc3d.load_installed_fonts()
|
||||||
|
|
||||||
Font.name_to_glyph_d = Font.generate_name_to_glyph_d()
|
Font.init()
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
|
@ -1615,7 +1611,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 {bl_info['name']}")
|
print(f"UNREGISTER {utils.prefix()}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
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}")
|
# print(f"available font: {f.font_name} {f.face_name}")
|
||||||
register_font_from_filepath(font_path)
|
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
|
"""Show a simple message box
|
||||||
|
|
||||||
|
@ -579,6 +581,13 @@ def ShowMessageBox(title = "Message Box", icon = 'INFO', message=""):
|
||||||
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):
|
||||||
|
@ -665,11 +674,21 @@ 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):
|
def prepare_text(font_name, face_name, text, allow_replacement=True):
|
||||||
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)
|
||||||
|
@ -761,9 +780,6 @@ 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':
|
||||||
|
@ -778,14 +794,33 @@ 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 = Font.get_glyph(text_properties.font_name,
|
glyph_tmp = Font.get_glyph(text_properties.font_name,
|
||||||
text_properties.face_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:
|
message=f"Glyph not found for font_name='{text_properties.font_name}' face_name='{text_properties.face_name}' glyph_id='{glyph_id}'"
|
||||||
# self.report({'ERROR'}, f"Glyph not found for {font_name} {face_name} {glyph_id}")
|
replaced = False
|
||||||
print(f"Glyph not found for {text_properties.font_name} {text_properties.face_name} {glyph_id}")
|
if glyph_id.isalpha():
|
||||||
continue
|
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
|
ob = None
|
||||||
obg = 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"
|
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
|
||||||
|
@ -837,10 +881,7 @@ 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:
|
||||||
|
@ -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()
|
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,6 +37,8 @@ name_to_glyph_d = {
|
||||||
"space": " ",
|
"space": " ",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
space_d = {}
|
||||||
|
|
||||||
known_misspellings = {
|
known_misspellings = {
|
||||||
# simple misspelling
|
# simple misspelling
|
||||||
"excent" : "accent",
|
"excent" : "accent",
|
||||||
|
@ -74,17 +76,44 @@ 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(f"{Path(__file__).parent}/glyphNamesToUnicode.txt") as f:
|
with open(filepath) as f:
|
||||||
for line in f:
|
for line in f:
|
||||||
if line[0] == '#':
|
if line[0] == '#':
|
||||||
continue
|
continue
|
||||||
(name, hexstr) = line.split(' ')
|
split = line.split(' ')
|
||||||
val = chr(int(hexstr, base=16))
|
if len(split) == 2:
|
||||||
d[name] = val
|
(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
|
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
|
||||||
|
@ -174,19 +203,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
|
||||||
|
|
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():
|
def get_version_minor():
|
||||||
return 0
|
return 0
|
||||||
def get_version_patch():
|
def get_version_patch():
|
||||||
return 3
|
return 4
|
||||||
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,6 +72,10 @@ 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
|
||||||
|
|
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