Compare commits

..

No commits in common. "main" and "v0.0.8" have entirely different histories.
main ... v0.0.8

10 changed files with 774 additions and 2086 deletions

1
.gitignore vendored
View file

@ -1,7 +1,6 @@
# python # python
__pycache__ __pycache__
venv venv
venv*
# vim # vim
*.swo *.swo

View file

@ -5,8 +5,10 @@
/ ___ \| |_) | |___ ___) | |_| | / ___ \| |_) | |___ ___) | |_| |
/_/ \_\____/ \____|____/|____/ /_/ \_\____/ \____|____/|____/
``` ```
v0.0.12 v0.0.8
Convenience addon to work with 3D typography in Blender and Cinema4D. Convenience tool to work with 3D typography in Blender and Cinema4D.
Install as you would normally install an addon.
Instructions for development in [CONTRIBUTING,md](./CONTRIBUTING.md). Instructions for development in [CONTRIBUTING,md](./CONTRIBUTING.md).

File diff suppressed because it is too large Load diff

1555
butils.py

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
from typing import Dict
from pathlib import Path from pathlib import Path
from typing import Dict, NamedTuple
# convenience dictionary for translating names to glyph ids # convenience dictionary for translating names to glyph ids
# note: overwritten/extended by the content of "glypNamesToUnicode.txt" # note: overwritten/extended by the content of "glypNamesToUnicode.txt"
@ -177,34 +177,6 @@ def register_font(font_name, face_name, glyphs_in_fontfile, filepath):
fonts[font_name].faces[face_name].filepaths.append(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): def add_glyph(font_name, face_name, glyph_id, glyph_object):
"""add_glyph adds a glyph to a FontFace """add_glyph adds a glyph to a FontFace
it creates the :class:`Font` and :class:`FontFace` if it does not exist yet it creates the :class:`Font` and :class:`FontFace` if it does not exist yet
@ -231,48 +203,7 @@ def add_glyph(font_name, face_name, glyph_id, glyph_object):
fonts[font_name].faces[face_name].loaded_glyphs.append(glyph_id) fonts[font_name].faces[face_name].loaded_glyphs.append(glyph_id)
def get_glyphs(font_name, face_name, glyph_id): def get_glyph(font_name, face_name, glyph_id, alternate=0):
"""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")
try:
print(fonts[font_name].faces.keys())
except:
print(fonts.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: str,
face_name: str,
glyph_id: str,
alternate: int = 0,
alternate_tolerant: bool = True,
):
"""add_glyph adds a glyph to a FontFace """add_glyph adds a glyph to a FontFace
it creates the :class:`Font` and :class:`FontFace` if it does not exist yet it creates the :class:`Font` and :class:`FontFace` if it does not exist yet
@ -282,53 +213,30 @@ def get_glyph(
:type face_name: str :type face_name: str
:param glyph_id: The ``glyph_id`` from the glyph you want :param glyph_id: The ``glyph_id`` from the glyph you want
:type glyph_id: str :type glyph_id: str
:param alternate: The ``alternate`` from the glyph you want
:type alternate: int
:param alternate_tolerant: Fetch an existing alternate if requested is out of bounds
:type glyph_id: bool
... ...
:return: returns the glyph object, or ``None`` if it does not exist :return: returns the glyph object, or ``None`` if it does not exist
:rtype: `Object` :rtype: `Object`
""" """
glyphs = get_glyphs(font_name, face_name, glyph_id) if not fonts.keys().__contains__(font_name):
# print(f"ABC3D::get_glyph: font name({font_name}) not found")
if len(glyphs) == 0: # print(fonts.keys())
print(
f"ABC3D::get_glyph: font({font_name}) face({face_name}) glyph({glyph_id})[{alternate}] not found"
)
return None return None
if len(glyphs) <= alternate: face = fonts[font_name].faces.get(face_name)
if alternate_tolerant:
alternate = 0
else:
print(
f"ABC3D::get_glyph: font({font_name}) face({face_name}) glyph({glyph_id})[{alternate}] not found"
)
return None
return glyphs[alternate]
def unloaded_glyph(font_name, face_name, glyph_id):
face = get_font_face(font_name, face_name)
if face is None: if face is 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")
return # print(fonts[font_name].faces.keys())
while True: return None
try:
fonts[font_name].faces[face_name].loaded_glyphs.remove(glyph_id)
del fonts[font_name].faces[face_name].glyphs[glyph_id]
except ValueError:
break
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
class GlyphsAvailability(NamedTuple): return fonts[font_name].faces[face_name].glyphs.get(glyph_id)[alternate]
loaded: str
missing: str
unloaded: str
filepaths: list[str]
def test_glyphs_availability(font_name, face_name, text): def test_glyphs_availability(font_name, face_name, text):
@ -337,24 +245,24 @@ def test_glyphs_availability(font_name, face_name, text):
not fonts.keys().__contains__(font_name) not fonts.keys().__contains__(font_name)
or fonts[font_name].faces.get(face_name) is None or fonts[font_name].faces.get(face_name) is None
): ):
return GlyphsAvailability("", "", "", []) return "", "", text # <loaded>, <missing>, <maybe>
loaded = [] loaded = []
missing = [] missing = []
unloaded = [] maybe = []
for c in text: for c in text:
if c in fonts[font_name].faces[face_name].loaded_glyphs: if c in fonts[font_name].faces[face_name].loaded_glyphs:
loaded.append(c) loaded.append(c)
elif c in fonts[font_name].faces[face_name].glyphs_in_fontfile: elif c in fonts[font_name].faces[face_name].glyphs_in_fontfile:
unloaded.append(c) maybe.append(c)
else: else:
if c not in fonts[font_name].faces[face_name].missing_glyphs: if c not in fonts[font_name].faces[face_name].missing_glyphs:
fonts[font_name].faces[face_name].missing_glyphs.append(c) fonts[font_name].faces[face_name].missing_glyphs.append(c)
missing.append(c) missing.append(c)
return GlyphsAvailability( return (
"".join(loaded), "".join(loaded),
"".join(missing), "".join(missing),
"".join(unloaded), "".join(maybe),
fonts[font_name].faces[face_name].filepaths, fonts[font_name].faces[face_name].filepaths,
) )
@ -380,10 +288,15 @@ def test_availability(font_name, face_name, text):
return MISSING_FONT return MISSING_FONT
if fonts[font_name].faces.get(face_name) is None: if fonts[font_name].faces.get(face_name) is None:
return MISSING_FACE return MISSING_FACE
availability: GlyphsAvailability = test_glyphs_availability( loaded, missing, maybe, filepaths = test_glyphs_availability(
font_name, face_name, text font_name, face_name, text
) )
return availability return {
"loaded": loaded,
"missing": missing,
"maybe": maybe,
"filepaths": filepaths,
}
# holds all fonts # holds all fonts

View file

@ -4,20 +4,20 @@ space 0020 0.25
nbspace 00A0 0.25 nbspace 00A0 0.25
# ethi:wordspace 1361 # NOTE: has shape # ethi:wordspace 1361 # NOTE: has shape
enquad 2000 0.5 enquad 2000 0.5
emquad 2001 1.0 emquad 2001 1
enspace 2002 0.5 enspace 2002 0.5
emspace 2003 1.0 emspace 2003 1
threeperemspace 2004 3.0 threeperemspace 2004 3
fourperemspace 2005 4.0 fourperemspace 2005 4
sixperemspace 2006 6.0 sixperemspace 2006 6
figurespace 2007 1.0 figurespace 2007 1
punctuationspace 2008 1.0 punctuationspace 2008 1
thinspace 2009 0.1 thinspace 2009 0.1
hairspace 200A 0.05 hairspace 200A 0.05
zerowidthspace 200B 0.0 zerowidthspace 200B 0
narrownobreakspace 202F 0.1 narrownobreakspace 202F 0.1
mediummathematicalspace 205F 1.0 mediummathematicalspace 205F 1
cntr:space 2420 0.25 cntr:space 2420 0.25
ideographicspace 3000 1.0 ideographicspace 3000 1
# ideographichalffillspace 303F # NOTE: has shape # ideographichalffillspace 303F # NOTE: has shape
zerowidthnobreakspace FEFF 0.0 zerowidthnobreakspace FEFF 0

View file

@ -8,11 +8,11 @@ def get_version_minor():
def get_version_patch(): def get_version_patch():
return 12 return 8
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():
@ -23,6 +23,7 @@ import datetime
import time import time
def get_timestamp(): def get_timestamp():
return datetime.datetime.fromtimestamp(time.time()).strftime("%Y.%m.%d-%H:%M:%S") return datetime.datetime.fromtimestamp(time.time()).strftime("%Y.%m.%d-%H:%M:%S")
@ -81,10 +82,6 @@ def open_file_browser(directory):
# xdg-open *should* be supported by recent Gnome, KDE, Xfce # xdg-open *should* be supported by recent Gnome, KDE, Xfce
def LINE():
return sys._getframe(1).f_lineno
def printerr(*args, **kwargs): def printerr(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs) print(*args, file=sys.stderr, **kwargs)
@ -93,34 +90,6 @@ def removeNonAlphabetic(s):
return "".join([i for i in s if i.isalpha()]) return "".join([i for i in s if i.isalpha()])
import os
import pathlib
def can_create_path(path_str: str):
path = pathlib.Path(path_str).absolute().resolve()
tries = 0
maximum_tries = 1000
while True:
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
tries += 1
if tries > maximum_tries:
# always, always break out of while loops eventually
# IF you don't want to be here forever
break
# # 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

View file

@ -1,39 +1,33 @@
asttokens==3.0.0 astroid==3.3.5
attrs==25.3.0 attrs==24.2.0
bpy==4.4.0 black==24.10.0
cattrs==24.1.3 bpy==4.2.0
certifi==2025.4.26 cattrs==24.1.2
charset-normalizer==3.4.2 certifi==2024.8.30
Cython==3.1.1 charset-normalizer==3.4.0
decorator==5.2.1 click==8.1.7
docstring-to-markdown==0.17 Cython==3.0.11
executing==2.2.0 dill==0.3.9
docstring-to-markdown==0.15
flake8==7.1.1
idna==3.10 idna==3.10
importlib_metadata==8.7.0 isort==5.13.2
ipython==9.2.0 jedi==0.19.1
ipython_pygments_lexers==1.1.1 jedi-language-server==0.41.4
jedi==0.19.2
jedi-language-server==0.45.1
lsprotocol==2023.0.1 lsprotocol==2023.0.1
mathutils==3.3.0 mathutils==3.3.0
matplotlib-inline==0.1.7 mccabe==0.7.0
numpy==1.26.4 mypy-extensions==1.0.0
numpy==2.1.3
packaging==24.1
parso==0.8.4 parso==0.8.4
pexpect==4.9.0 pathspec==0.12.1
pluggy==1.6.0 platformdirs==4.3.6
prompt_toolkit==3.0.51 pycodestyle==2.12.1
ptyprocess==0.7.0 pyflakes==3.2.0
pure_eval==0.2.3
pygls==1.3.1 pygls==1.3.1
Pygments==2.19.1 pylint==3.3.1
python-jsonrpc-server==0.4.0
python-lsp-jsonrpc==1.1.2
requests==2.32.3 requests==2.32.3
stack-data==0.6.3 tomlkit==0.13.2
traitlets==5.14.3 urllib3==2.2.3
typing_extensions==4.13.2
ujson==5.10.0
urllib3==2.4.0
wcwidth==0.2.13
zipp==3.22.0
zstandard==0.23.0 zstandard==0.23.0

View file

@ -1,25 +0,0 @@
import bpy
from mathutils import *
from math import *
import abc3d.butils
v = 0
goal = 5.0
step = 0.1
speed = 1.0
C = bpy.context
obj = C.scene.objects['Cube']
curve = C.scene.objects['BézierCurve']
m = curve.matrix
def fun(distance):
obj.location = m @ abc3d.butils.calc_point_on_bezier_curve(curve,
distance,
output_tangent=True)
print(f"executed {distance}")
while v < goal:
bpy.app.timers.register(lambda: fun(v), first_interval=(v * speed))
v += step

View file

@ -1,115 +0,0 @@
import bpy
import abc3d
from abc3d import butils
from abc3d.common import Font
def get_text_properties_by_mom(mom):
scene = bpy.context.scene
abc3d_data = scene.abc3d_data
for text_properties in abc3d_data.available_texts:
if mom == text_properties.text_object:
return text_properties
return None
def isolate_objects(objects):
for area in bpy.context.window.screen.areas:
if area.type == "VIEW_3D":
with bpy.context.temp_override(
selected_objects=list(objects),
area=area,
refgion=[region for region in area.regions if region.type == "WINDOW"][
0
],
screen=bpy.context.window.screen,
):
# bpy.ops.view3d.view_selected()
bpy.ops.view3d.localview(frame_selected=True)
break
def main():
# create a curve
bpy.ops.curve.primitive_bezier_curve_add(
radius=1,
enter_editmode=False,
align="WORLD",
location=(0, 0, 0),
scale=(1, 1, 1),
)
# new curve is active object
mom = bpy.context.active_object
# make sure
print(f"MOM: {mom.name}")
fonts = Font.get_loaded_fonts_and_faces()
if len(fonts) == 0:
print("no fonts! what?")
return
font_name = fonts[0][0]
face_name = fonts[0][1]
font = f"{font_name} {face_name}"
isolate_objects([mom])
bpy.ops.abc3d.placetext(
font_name=font_name,
face_name=face_name,
font=font,
text="SOMETHING SOMETHING BROKEN ARMS",
letter_spacing=0,
font_size=1,
offset=0,
translation=(0, 0, 0),
orientation=(1.5708, 0, 0),
)
def change_text(font_name="", face_name="", text=""):
print(f"change_text to '{text}'")
text_properties = get_text_properties_by_mom(mom)
if font_name != "":
text_properties["font_name"] = font_name
if face_name != "":
text_properties["face_name"] = face_name
if text != "":
text_properties.text = text
else:
text_properties.text = text_properties.text
return None
def unload(glyph_id):
print(f"unload glyph '{glyph_id}'")
butils.unload_unused_glyph(font_name, face_name, glyph_id)
return None
def unload_all():
print(f"unload glyph all unused glyphs")
butils.unload_unused_glyphs()
return None
bpy.app.timers.register(lambda: change_text(text="SOMETHING"), first_interval=0)
bpy.app.timers.register(lambda: change_text(text="LOLSS"), first_interval=2)
bpy.app.timers.register(lambda: change_text(text="LOLAA"), first_interval=3)
bpy.app.timers.register(lambda: change_text(text="WHAT"), first_interval=4)
bpy.app.timers.register(lambda: change_text(text="LOL"), first_interval=5)
bpy.app.timers.register(lambda: unload("A"), first_interval=10)
bpy.app.timers.register(lambda: unload_all(), first_interval=12)
bpy.app.timers.register(lambda: change_text(text="LOLM"), first_interval=16)
bpy.app.timers.register(lambda: change_text(text="ZHE END"), first_interval=20)
bpy.app.timers.register(
lambda: change_text(font_name="NM_Origin", face_name="Tender"),
first_interval=30,
)
bpy.app.timers.register(lambda: unload_all(), first_interval=42)
main()