Compare commits

...

49 commits
v0.0.6 ... main

Author SHA1 Message Date
a5602a6095 bump version number to v0.0.12 2025-06-05 13:20:39 +02:00
cd99362bb1 [feature] spline animation 2025-06-05 10:59:14 +02:00
f02f8fc2f0 depsgraph update set text
this updates on animation
2025-06-05 10:57:53 +02:00
e95266afc9 [fix] transfer text properties 2025-06-05 10:56:33 +02:00
58e0df3427 first step spline animation 2025-06-04 21:28:08 +02:00
7de8fcc5d1 refactor ensure glyphs + alternates 2025-06-04 14:47:09 +02:00
14d1b7a160 [fix] glyph receives text_id 2025-06-01 16:32:22 +02:00
59edb2e786 bump version to v0.0.11 2025-05-31 20:01:41 +02:00
bb0a5a4a2c add testing scripts 2025-05-31 19:57:11 +02:00
6160b99c93 [fix] recursive selections 2025-05-31 19:53:37 +02:00
d61607c75d [fix] deletion fixes+
and some smaller cosmetic changes
2025-05-31 17:47:47 +02:00
8470425d20 potential fix
i cannot imagine how the while loop would go on forever, but hard limits
are nice
2025-05-31 17:16:59 +02:00
01fcb60e31 fix version string 2025-05-31 17:11:07 +02:00
7a43cfaf2f [feature] unload glyphs and refresh fonts 2025-05-31 16:31:50 +02:00
2dcd4e7a2c [feature] unload glyphs 2025-05-31 16:13:16 +02:00
9423659153 update requirements.txt 2025-05-31 16:12:40 +02:00
19f4bf586f bump version to v0.0.10 2025-05-29 19:34:15 +02:00
04229fbc31 [fix] fix bezier when individual handles sit on points
bonus: prevent eternal while loop
2025-05-29 19:32:56 +02:00
963d89daf9 useful comment 2025-05-29 19:29:56 +02:00
3ef2ae934d [optimization] skip bezier
skip bezier if all handles sit on their points
2025-05-29 19:29:34 +02:00
8965ab11eb [feature] print line number 2025-05-29 19:28:35 +02:00
513497d492 better notices 2025-05-29 15:39:26 +02:00
335ab1face stabilizing, user experience
use class for glyph availability
use isinstance instead of type
better user experience when export directory does not exist
2025-05-29 15:27:24 +02:00
777644e509 bump version to v0.0.9 2025-05-26 06:57:13 +02:00
e14251523b cleanup prints 2025-05-26 06:55:29 +02:00
8f3d58aad0 transfer glyph transforms on duplication 2025-05-25 22:00:54 +02:00
2ace31a246 use get_text_properties instead of id as index 2025-05-25 20:41:00 +02:00
88cfaf3be7 detect textobject and allow primitive duplication 2025-05-25 20:36:46 +02:00
10e57dd46a depsgraph detect texts
implementing in depsgraph allows for duplication
2025-05-25 15:35:52 +02:00
c27cf41368 introduce detect_text() and friends 2025-05-25 14:16:15 +02:00
7a034efd1c all floats 2025-05-25 14:15:42 +02:00
3ea2f0e304 ignore more venvs 2025-05-25 14:15:21 +02:00
840fdf1ca4 bump version to v0.0.8 2025-05-24 15:25:12 +02:00
7b4e65cbb7 check for None 2025-05-24 15:22:01 +02:00
4113343e79 move imports to top 2025-05-24 15:18:20 +02:00
e21ecaef0a fix blender import path 4.3+ 2025-05-24 14:54:24 +02:00
9c77139dcd Merge branch 'main' into dev 2025-05-24 14:38:22 +02:00
13b5a4dd88 bump version to v0.0.7 2025-05-23 11:37:54 +02:00
49699db309 regenerate if needed in update_callback 2025-05-23 11:33:38 +02:00
7ebe913e49 fix rendering crashes
1) introduce can_regenerate so we only regenerate when necessary
2) no notifications of missing glyphs when rendering
3) use frame_change_pre instead of post
2025-05-20 19:24:43 +02:00
2422d0cf09 clean startup 2025-05-20 19:22:00 +02:00
d6dfbfa5a1 cleanup 2025-05-20 19:21:32 +02:00
5bd78a3fc1 Merge branch 'main' into dev 2025-01-19 16:05:03 +01:00
1e54a10341 Merge branch 'main' into dev 2024-12-07 15:16:14 +01:00
23624ea1eb bump version v0.0.3 2024-12-07 15:07:11 +01:00
42c4a33801 manual positioning
closes #1
2024-12-07 14:57:33 +01:00
35d864b9b8 remove outdated comments 2024-12-07 14:28:37 +01:00
a2c4ba60f2 cleanup 2024-11-21 15:04:08 +01:00
7e2eeeeec1 remove right panel 2024-11-21 14:59:26 +01:00
11 changed files with 2130 additions and 811 deletions

1
.gitignore vendored
View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -4,11 +4,10 @@ from bpy.props import (
BoolProperty,
EnumProperty,
IntProperty,
FloatProperty,
CollectionProperty,
)
from bpy.types import Operator
from bpy_extras.io_utils import ImportHelper, ExportHelper
from bpy_extras.io_utils import ImportHelper
from io_scene_gltf2 import ConvertGLTF2_Base
import importlib
@ -16,20 +15,31 @@ import importlib
if "Font" in locals():
importlib.reload(Font)
else:
from .common import Font
pass
if "utils" in locals():
importlib.reload(utils)
else:
from .common import utils
try:
from io_scene_gltf2.io.imp.gltf2_io_gltf import glTFImporter, ImportError
from io_scene_gltf2.blender.imp.gltf2_blender_gltf import BlenderGlTF
from io_scene_gltf2.blender.imp.gltf2_blender_vnode import VNode, compute_vnodes
from io_scene_gltf2.blender.com.gltf2_blender_extras import set_extras
from io_scene_gltf2.blender.imp.gltf2_blender_node import BlenderNode
except (ModuleNotFoundError, ImportError):
from io_scene_gltf2.io.imp.gltf2_io_gltf import glTFImporter, ImportError
from io_scene_gltf2.blender.imp.blender_gltf import BlenderGlTF
from io_scene_gltf2.blender.imp.vnode import VNode, compute_vnodes
from io_scene_gltf2.blender.com.extras import set_extras
from io_scene_gltf2.blender.imp.node import BlenderNode
# taken from blender_git/blender/scripts/addons/io_scene_gltf2/__init__.py
def get_font_faces_in_file(filepath):
from io_scene_gltf2.io.imp.gltf2_io_gltf import glTFImporter, ImportError
try:
import_settings = {"import_user_extensions": []}
gltf_importer = glTFImporter(filepath, import_settings)
@ -50,7 +60,7 @@ def get_font_faces_in_file(filepath):
out.append(node.extras)
return out
except ImportError as e:
except ImportError:
return None
@ -60,7 +70,7 @@ def get_font_faces_in_file(filepath):
class GetFontFacesInFile(Operator, ImportHelper):
"""Load a glTF 2.0 font and check which faces are in there"""
bl_idname = f"abc3d.check_font_gltf"
bl_idname = "abc3d.check_font_gltf"
bl_label = "Check glTF 2.0 Font"
bl_options = {"REGISTER", "UNDO"}
@ -77,7 +87,6 @@ class GetFontFacesInFile(Operator, ImportHelper):
def check_gltf2(self, context):
import os
import sys
if self.files:
# Multiple file check
@ -100,7 +109,7 @@ class GetFontFacesInFile(Operator, ImportHelper):
class ImportGLTF2(Operator, ConvertGLTF2_Base, ImportHelper):
"""Load a glTF 2.0 font"""
bl_idname = f"abc3d.import_font_gltf"
bl_idname = "abc3d.import_font_gltf"
bl_label = "Import glTF 2.0 Font"
bl_options = {"REGISTER", "UNDO"}
@ -285,11 +294,6 @@ class ImportGLTF2(Operator, ConvertGLTF2_Base, ImportHelper):
def unit_import(self, filename, import_settings):
import time
from io_scene_gltf2.io.imp.gltf2_io_gltf import glTFImporter, ImportError
from io_scene_gltf2.blender.imp.gltf2_blender_gltf import BlenderGlTF
from io_scene_gltf2.blender.imp.gltf2_blender_vnode import VNode, compute_vnodes
from io_scene_gltf2.blender.com.gltf2_blender_extras import set_extras
from io_scene_gltf2.blender.imp.gltf2_blender_node import BlenderNode
try:
gltf = glTFImporter(filename, import_settings)

1565
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 typing import Dict, NamedTuple
# convenience dictionary for translating names to glyph ids
# note: overwritten/extended by the content of "glypNamesToUnicode.txt"
@ -163,7 +163,7 @@ class Font:
def register_font(font_name, face_name, glyphs_in_fontfile, filepath):
if not fonts.keys().__contains__(font_name):
fonts[font_name] = Font({})
if fonts[font_name].faces.get(face_name) == None:
if fonts[font_name].faces.get(face_name) is None:
fonts[font_name].faces[face_name] = FontFace({})
fonts[font_name].faces[face_name].glyphs_in_fontfile = glyphs_in_fontfile
else:
@ -177,6 +177,34 @@ def register_font(font_name, face_name, glyphs_in_fontfile, 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):
"""add_glyph adds a glyph to a FontFace
it creates the :class:`Font` and :class:`FontFace` if it does not exist yet
@ -193,9 +221,9 @@ def add_glyph(font_name, face_name, glyph_id, glyph_object):
if not fonts.keys().__contains__(font_name):
fonts[font_name] = Font({})
if fonts[font_name].faces.get(face_name) == None:
if fonts[font_name].faces.get(face_name) is None:
fonts[font_name].faces[face_name] = FontFace({})
if fonts[font_name].faces[face_name].glyphs.get(glyph_id) == None:
if fonts[font_name].faces[face_name].glyphs.get(glyph_id) is None:
fonts[font_name].faces[face_name].glyphs[glyph_id] = []
fonts[font_name].faces[face_name].glyphs.get(glyph_id).append(glyph_object)
@ -203,7 +231,48 @@ def add_glyph(font_name, face_name, glyph_id, glyph_object):
fonts[font_name].faces[face_name].loaded_glyphs.append(glyph_id)
def get_glyph(font_name, face_name, glyph_id, alternate=0):
def get_glyphs(font_name, face_name, glyph_id):
"""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
it creates the :class:`Font` and :class:`FontFace` if it does not exist yet
@ -213,56 +282,79 @@ def get_glyph(font_name, face_name, glyph_id, alternate=0):
:type face_name: str
:param glyph_id: The ``glyph_id`` from the glyph you want
: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
:rtype: `Object`
"""
if not fonts.keys().__contains__(font_name):
# print(f"ABC3D::get_glyph: font name({font_name}) not found")
# print(fonts.keys())
glyphs = get_glyphs(font_name, face_name, glyph_id)
if len(glyphs) == 0:
print(
f"ABC3D::get_glyph: font({font_name}) face({face_name}) glyph({glyph_id})[{alternate}] not found"
)
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())
return None
if len(glyphs) <= alternate:
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
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")
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
return glyphs[alternate]
return fonts[font_name].faces[face_name].glyphs.get(glyph_id)[alternate]
def unloaded_glyph(font_name, face_name, glyph_id):
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")
return
while True:
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
class GlyphsAvailability(NamedTuple):
loaded: str
missing: str
unloaded: str
filepaths: list[str]
def test_glyphs_availability(font_name, face_name, text):
# maybe there is NOTHING yet
if (
not fonts.keys().__contains__(font_name)
or fonts[font_name].faces.get(face_name) == None
or fonts[font_name].faces.get(face_name) is None
):
return "", "", text # <loaded>, <missing>, <maybe>
return GlyphsAvailability("", "", "", [])
loaded = []
missing = []
maybe = []
unloaded = []
for c in text:
if c in fonts[font_name].faces[face_name].loaded_glyphs:
loaded.append(c)
elif c in fonts[font_name].faces[face_name].glyphs_in_fontfile:
maybe.append(c)
unloaded.append(c)
else:
if c not in fonts[font_name].faces[face_name].missing_glyphs:
fonts[font_name].faces[face_name].missing_glyphs.append(c)
missing.append(c)
return (
return GlyphsAvailability(
"".join(loaded),
"".join(missing),
"".join(maybe),
"".join(unloaded),
fonts[font_name].faces[face_name].filepaths,
)
@ -286,17 +378,12 @@ MISSING_FACE = 1
def test_availability(font_name, face_name, text):
if not fonts.keys().__contains__(font_name):
return MISSING_FONT
if fonts[font_name].faces.get(face_name) == None:
if fonts[font_name].faces.get(face_name) is None:
return MISSING_FACE
loaded, missing, maybe, filepaths = test_glyphs_availability(
availability: GlyphsAvailability = test_glyphs_availability(
font_name, face_name, text
)
return {
"loaded": loaded,
"missing": missing,
"maybe": maybe,
"filepaths": filepaths,
}
return availability
# holds all fonts

View file

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

View file

@ -8,11 +8,11 @@ def get_version_minor():
def get_version_patch():
return 6
return 12
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():
@ -23,7 +23,6 @@ import datetime
import time
def get_timestamp():
return datetime.datetime.fromtimestamp(time.time()).strftime("%Y.%m.%d-%H:%M:%S")
@ -82,6 +81,10 @@ def open_file_browser(directory):
# xdg-open *should* be supported by recent Gnome, KDE, Xfce
def LINE():
return sys._getframe(1).f_lineno
def printerr(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)
@ -90,6 +93,34 @@ def removeNonAlphabetic(s):
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
# 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

View file

@ -1,33 +1,39 @@
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
asttokens==3.0.0
attrs==25.3.0
bpy==4.4.0
cattrs==24.1.3
certifi==2025.4.26
charset-normalizer==3.4.2
Cython==3.1.1
decorator==5.2.1
docstring-to-markdown==0.17
executing==2.2.0
idna==3.10
isort==5.13.2
jedi==0.19.1
jedi-language-server==0.41.4
importlib_metadata==8.7.0
ipython==9.2.0
ipython_pygments_lexers==1.1.1
jedi==0.19.2
jedi-language-server==0.45.1
lsprotocol==2023.0.1
mathutils==3.3.0
mccabe==0.7.0
mypy-extensions==1.0.0
numpy==2.1.3
packaging==24.1
matplotlib-inline==0.1.7
numpy==1.26.4
parso==0.8.4
pathspec==0.12.1
platformdirs==4.3.6
pycodestyle==2.12.1
pyflakes==3.2.0
pexpect==4.9.0
pluggy==1.6.0
prompt_toolkit==3.0.51
ptyprocess==0.7.0
pure_eval==0.2.3
pygls==1.3.1
pylint==3.3.1
Pygments==2.19.1
python-jsonrpc-server==0.4.0
python-lsp-jsonrpc==1.1.2
requests==2.32.3
tomlkit==0.13.2
urllib3==2.2.3
stack-data==0.6.3
traitlets==5.14.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

View file

@ -0,0 +1,25 @@
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

@ -0,0 +1,115 @@
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()