Compare commits
23 commits
Author | SHA1 | Date | |
---|---|---|---|
a5602a6095 | |||
cd99362bb1 | |||
f02f8fc2f0 | |||
e95266afc9 | |||
58e0df3427 | |||
7de8fcc5d1 | |||
14d1b7a160 | |||
59edb2e786 | |||
bb0a5a4a2c | |||
6160b99c93 | |||
d61607c75d | |||
8470425d20 | |||
01fcb60e31 | |||
7a43cfaf2f | |||
2dcd4e7a2c | |||
9423659153 | |||
19f4bf586f | |||
04229fbc31 | |||
963d89daf9 | |||
3ef2ae934d | |||
8965ab11eb | |||
513497d492 | |||
335ab1face |
8 changed files with 1681 additions and 691 deletions
|
@ -5,10 +5,8 @@
|
|||
/ ___ \| |_) | |___ ___) | |_| |
|
||||
/_/ \_\____/ \____|____/|____/
|
||||
```
|
||||
v0.0.9
|
||||
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).
|
||||
|
|
770
__init__.py
770
__init__.py
File diff suppressed because it is too large
Load diff
143
common/Font.py
143
common/Font.py
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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,30 +282,53 @@ 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 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
|
||||
|
||||
return glyphs[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")
|
||||
# print(fonts[font_name].faces.keys())
|
||||
return 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
|
||||
|
||||
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
|
||||
|
||||
return fonts[font_name].faces[face_name].glyphs.get(glyph_id)[alternate]
|
||||
class GlyphsAvailability(NamedTuple):
|
||||
loaded: str
|
||||
missing: str
|
||||
unloaded: str
|
||||
filepaths: list[str]
|
||||
|
||||
|
||||
def test_glyphs_availability(font_name, face_name, text):
|
||||
|
@ -245,24 +337,24 @@ def test_glyphs_availability(font_name, face_name, text):
|
|||
not fonts.keys().__contains__(font_name)
|
||||
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,
|
||||
)
|
||||
|
||||
|
@ -288,15 +380,10 @@ def test_availability(font_name, face_name, text):
|
|||
return MISSING_FONT
|
||||
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
|
||||
|
|
|
@ -8,11 +8,11 @@ def get_version_minor():
|
|||
|
||||
|
||||
def get_version_patch():
|
||||
return 9
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
25
testing_scripts/bezier_distance.py
Normal file
25
testing_scripts/bezier_distance.py
Normal 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
|
115
testing_scripts/unload_glyphs.py
Normal file
115
testing_scripts/unload_glyphs.py
Normal 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()
|
Loading…
Add table
Add a link
Reference in a new issue