font3d_blender_addon/__init__.py
themancalledjakob f6b1649c71 smoother install font & depsgraph update
when we set a text, we don't want our manual depsgraph update, because
this would trigger another setting of the text, which would trigger the
manual depsgraph update, which would trigger another setting of the
text, which would trigger the manual depsgraph update, which will
eventually be too much.

So, we ignore the depsgraph update n-times, where "n = n-letters + 1".
worst side effect: might not update the text automatically in edge
cases.
2024-11-15 20:24:06 +01:00

1687 lines
61 KiB
Python

# SPDX-License-Identifier: GPL-2.0-only
"""
A 3D font helper
"""
import os
from bpy.app.handlers import persistent
from bpy.types import Panel
import functools
import io
import bpy
import importlib
bl_info = {
"name": "ABC3D",
"author": "Jakob Schlötter, Studio Pointer*",
"version": (0, 0, 2),
"blender": (4, 1, 0),
"location": "VIEW3D",
"description": "Convenience addon for 3D fonts",
"category": "Typography",
}
# make sure that modules are reloadable
# when registering
# handy for development
# first import dependencies for the method
if "Font" in locals():
importlib.reload(Font)
importlib.reload(utils)
importlib.reload(butils)
importlib.reload(bimport)
importlib.reload(addon_updater_ops)
else:
from .common import Font
from .common import utils
from . import butils
from . import bimport
from . import addon_updater_ops
def getPreferences(context):
preferences = context.preferences
return preferences.addons[__name__].preferences
@addon_updater_ops.make_annotations
class ABC3D_addonPreferences(bpy.types.AddonPreferences):
"""ABC3D Addon Preferences
These are the preferences at Edit/Preferences/Add-ons"""
bl_idname = __name__
# Addon updater preferences.
auto_check_update = bpy.props.BoolProperty(
name="Auto-check for Update",
description="If enabled, auto-check for updates using an interval",
default=False)
updater_interval_months = bpy.props.IntProperty(
name='Months',
description="Number of months between checking for updates",
default=0,
min=0)
updater_interval_days = bpy.props.IntProperty(
name='Days',
description="Number of days between checking for updates",
default=7,
min=0,
max=31)
updater_interval_hours = bpy.props.IntProperty(
name='Hours',
description="Number of hours between checking for updates",
default=0,
min=0,
max=23)
updater_interval_minutes = bpy.props.IntProperty(
name='Minutes',
description="Number of minutes between checking for updates",
default=0,
min=0,
max=59)
def get_default_assets_dir():
return bpy.utils.user_resource(
'DATAFILES',
path=f"{__name__}",
create=True)
def on_change_assets_dir(self, context):
if not os.path.isdir(self.assets_dir):
butils.ShowMessageBox(
title=f"{__name__} Warning",
icon="ERROR",
message=("Chosen directory does not exist.",
"Please, reset to default, create it or chose another one."))
elif not os.access(self.assets_dir, os.W_OK):
butils.ShowMessageBox(
title=f"{__name__} Warning",
icon="ERROR",
message=("Chosen directory is not writable.",
"Please reset to default or chose another one."))
print(f"{__name__}: change assets_dir to {self.assets_dir}")
assets_dir: bpy.props.StringProperty(
name="Assets Folder",
subtype='DIR_PATH',
default=get_default_assets_dir(),
update=on_change_assets_dir,
)
def draw(self, context):
layout = self.layout
layout.label(text="Directory for storage of fonts and other assets:")
layout.prop(self, "assets_dir")
# Works best if a column, or even just self.layout.
mainrow = layout.row()
col = mainrow.column()
# Updater draw function, could also pass in col as third arg.
addon_updater_ops.update_settings_ui(self, context)
class ABC3D_available_font(bpy.types.PropertyGroup):
font_name: bpy.props.StringProperty(name="")
face_name: bpy.props.StringProperty(name="")
class ABC3D_glyph_properties(bpy.types.PropertyGroup):
glyph_id: bpy.props.StringProperty(maxlen=1)
glyph_object: bpy.props.PointerProperty(type=bpy.types.Object)
letter_spacing: bpy.props.FloatProperty(
name="Letter Spacing",
description="Letter Spacing",
)
class ABC3D_text_properties(bpy.types.PropertyGroup):
def font_items_callback(self, context):
items = []
for f in Font.get_loaded_fonts_and_faces():
items.append((f"{f[0]} {f[1]}", f"{f[0]} {f[1]}", ""))
return items
def font_default_callback(self, context):
d = context.scene.abc3d_data
items = self.font_items_callback(context)
if len(d.available_fonts) > 0:
if len(d.available_fonts) > d.active_text_index:
f = d.available_fonts[d.active_text_index]
return 0 # f"{f.font_name} {f.face_name}"
else:
f = d.available_fonts[0]
return 0 # f"{f.font_name} {f.face_name}"
if type(self.font_name) != type(None) and type(self.face_name) != type(None):
return 0 # f"{self.font_name} {self.face_name}"
else:
return 0 # ""
def glyphs_update_callback(self, context):
butils.prepare_text(self.font_name,
self.face_name,
self.text)
butils.set_text_on_curve(self)
def update_callback(self, context):
butils.set_text_on_curve(self)
def font_update_callback(self, context):
font_name, face_name = self.font.split(" ")
self["font_name"] = font_name
self["face_name"] = face_name
self.glyphs_update_callback(self)
text_id: bpy.props.IntProperty()
font: bpy.props.EnumProperty(
items=font_items_callback,
update=font_update_callback,
)
font_name: bpy.props.StringProperty(
update=glyphs_update_callback
)
face_name: bpy.props.StringProperty(
update=glyphs_update_callback
)
text_object: bpy.props.PointerProperty(type=bpy.types.Object)
text: bpy.props.StringProperty(
update=glyphs_update_callback
)
letter_spacing: bpy.props.FloatProperty(
update=update_callback,
name="Letter Spacing",
description="Letter Spacing",
options={'ANIMATABLE'},
step=0.01,
)
orientation: bpy.props.FloatVectorProperty(
update=update_callback,
name="Orientation",
default=(1.5707963267948966, 0.0, 0.0), # 90 degrees in radians
subtype='EULER',
)
translation: bpy.props.FloatVectorProperty(
update=update_callback,
name="Translation",
default=(0.0, 0.0, 0.0),
subtype='TRANSLATION',
)
font_size: bpy.props.FloatProperty(
update=update_callback,
name="Font Size",
default=1.0,
subtype='NONE',
)
offset: bpy.props.FloatProperty(
update=update_callback,
name="Offset",
default=0.0,
subtype='NONE',
)
compensate_curvature: bpy.props.BoolProperty(
update=update_callback,
name="Compensate Curvature",
description="Fixes curvature spacing issues for simple curves, don't use on curve with tiny loops.",
default=True,
)
ignore_orientation: bpy.props.BoolProperty(
update=update_callback,
name="Ignore Curve Orientation",
default=False,
)
distribution_type: bpy.props.StringProperty()
glyphs: bpy.props.CollectionProperty(type=ABC3D_glyph_properties)
# TODO: simply, merge, cut cut cut
class ABC3D_data(bpy.types.PropertyGroup):
available_fonts: bpy.props.CollectionProperty(
type=ABC3D_available_font, name="Available fonts")
def active_font_index_update(self, context):
if len(self.available_fonts) <= self.active_font_index:
self.active_font_index = len(self.available_fonts) - 1
active_font_index: bpy.props.IntProperty(
default=-1,
update=active_font_index_update,
)
available_texts: bpy.props.CollectionProperty(
type=ABC3D_text_properties, name="Available texts")
def active_text_index_update(self, context):
if self.active_text_index != -1:
o = self.available_texts[self.active_text_index].text_object
# active_text_index changed. so let's update the selection
# check if it is already selected
# or perhaps one of the glyphs
if not o.select_get() and not len([c for c in o.children if c.select_get()]) > 0:
bpy.ops.object.select_all(action="DESELECT")
o.select_set(True)
bpy.context.view_layer.objects.active = o
# else:
# print("already selected")
active_text_index: bpy.props.IntProperty(update=active_text_index_update)
# def font_path_update_callback(self, context):
# butils.ShowMessageBox("Friendly Reminder", message="do not forget to click on 'Install new font'")
# bpy.ops.abc3d.install_font()
# stupid hack for Mac OS
font_path: bpy.props.StringProperty(
name="Font path",
description="Install a *.glb or *.gltf fontfile from disk",
default="",
maxlen=1024,
# update=font_path_update_callback,
subtype="FILE_PATH")
export_dir: bpy.props.StringProperty(
name="Export Directory",
description=f"The directory in which we will export fonts.\nIf it is blank, we will export to the addon assets path.\nThis is where the fonts are installed.",
subtype="DIR_PATH")
class ABC3D_UL_fonts(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
# avoids renaming the item by accident
layout.label(text=f"{index}: {item.font_name} {item.face_name}")
def invoke(self, context, event):
pass
class ABC3D_UL_texts(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
split = layout.split(factor=0.3)
split.label(text="Id: %d" % (item.text_id))
# avoids renaming the item by accident
split.label(text=f"{item.text}")
def invoke(self, context, event):
pass
class ABC3D_PT_Panel(bpy.types.Panel):
bl_label = f"{__name__} panel"
bl_category = "ABC3D"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
def draw(self, context):
layout = self.layout
icon = 'NONE'
if len(context.scene.abc3d_data.available_fonts) == 0:
icon = 'ERROR'
layout.row().label(text='no fonts loaded yet')
layout.operator(f"{__name__}.install_font", text='Install new font')
layout.operator(f"{__name__}.load_installed_fonts",
text="load installed fonts", icon=icon)
layout.operator(f"{__name__}.open_asset_directory",
text="open asset directory", icon='FILEBROWSER')
# class ABC3D_PT_LoadFontPanel(bpy.types.Panel):
# bl_label = "Install a new font"
# bl_parent_id = "ABC3D_PT_Panel"
# bl_category = "ABC3D"
# bl_space_type = "VIEW_3D"
# bl_region_type = "UI"
# def draw(self, context):
# layout = self.layout
# wm = context.window_manager
# scene = context.scene
# abc3d_data = scene.abc3d_data
# box = layout.box()
# box.row().label(text="1. Select fontfile")
# box.row().prop(context.scene.abc3d_data, "font_path")
# box.row().label(text="2. Install it:")
# box.row().operator(f"{__name__}.install_font", text='Install new font')
class ABC3D_PT_FontList(bpy.types.Panel):
bl_label = "Font List"
bl_parent_id = "ABC3D_PT_Panel"
bl_category = "ABC3D"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
def draw(self, context):
layout = self.layout
wm = context.window_manager
scene = context.scene
abc3d_data = scene.abc3d_data
layout.label(text="Available Fonts")
layout.template_list("ABC3D_UL_fonts", "", abc3d_data,
"available_fonts", abc3d_data, "active_font_index")
if abc3d_data.active_font_index >= 0:
available_font = abc3d_data.available_fonts[abc3d_data.active_font_index]
font_name = available_font.font_name
face_name = available_font.face_name
available_glyphs = sorted(
Font.fonts[font_name].faces[face_name].glyphs_in_fontfile)
loaded_glyphs = sorted(
Font.fonts[font_name].faces[face_name].loaded_glyphs)
box = layout.box()
box.row().label(text=f"Font Name: {font_name}")
box.row().label(text=f"Face Name: {face_name}")
n = 16
n_rows = int(len(available_glyphs) / n)
box.row().label(text=f"Glyphs:")
subbox = box.box()
for i in range(0, n_rows + 1):
text = ''.join([f"{u}" for ui, u in enumerate(
available_glyphs) if ui < (i+1) * n and ui >= i * n])
scale_y = 0.5
row = subbox.row()
row.scale_y = scale_y
row.alignment = 'CENTER'
row.label(text=text)
n_rows = int(len(loaded_glyphs) / n)
box.row().label(text=f"Loaded/Used Glyphs:")
subbox = box.box()
for i in range(0, n_rows + 1):
text = ''.join([f"{u}" for ui, u in enumerate(
loaded_glyphs) if ui < (i+1) * n and ui >= i * n])
scale_y = 0.5
row = subbox.row()
row.scale_y = scale_y
row.label(text=text)
row = layout.row()
oper = row.operator(f"{__name__}.load_font",
text='Load all glyphs in memory')
oper.font_name = font_name
oper.face_name = face_name
class ABC3D_PT_TextPlacement(bpy.types.Panel):
bl_label = "Place Text"
bl_parent_id = "ABC3D_PT_Panel"
bl_category = "ABC3D"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
can_place = False
@classmethod
def poll(self, context):
if type(context.active_object) != type(None) and context.active_object.type == 'CURVE':
self.can_place = True
else:
self.can_place = False
return True
def draw(self, context):
layout = self.layout
wm = context.window_manager
scene = context.scene
abc3d_data = scene.abc3d_data
placerow = layout.row()
placerow.enabled = self.can_place
placerow.operator(f"{__name__}.placetext", text='Place Text')
if not self.can_place:
layout.label(text="Cannot place Text.")
layout.label(text="Select a curve as active object.")
class ABC3D_PT_TextManagement(bpy.types.Panel):
bl_label = "Text Management"
bl_parent_id = "ABC3D_PT_Panel"
bl_category = "ABC3D"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_options = {"DEFAULT_CLOSED"}
# TODO: perhaps this should be done in a periodic timer
@classmethod
def poll(self, context):
scene = context.scene
abc3d_data = scene.abc3d_data
# TODO: update available_texts
def update():
if bpy.context.screen.is_animation_playing:
return
active_text_index = -1
remove_list = []
for i, t in enumerate(abc3d_data.available_texts):
if type(t.text_object) == type(None):
remove_list.append(i)
continue
remove_me = True
for c in t.text_object.children:
if len(c.users_collection) > 0 and (c.get(f"{utils.prefix()}_linked_textobject")) != type(None) and c.get(f"{utils.prefix()}_linked_textobject") == t.text_id:
remove_me = False
# not sure how to solve this reliably atm,
# we need to reassign the glyph, but also get the proper properties from glyph_properties
# these might be there in t.glyphs, but linked to removed objects
# or they might be lost
if type(next((g for g in t.glyphs if type(g.glyph_object) == type(None)), None)) == type(None):
g = next((g for g in t.glyphs if type(
g.glyph_object) == type(None)), None)
# for g in t.glyphs:
# if type(g) == type(None):
# print("IS NONE")
# if type(g.glyph_object) == type(None):
# print("go IS NONE")
# else:
# if g.glyph_object == c:
# # print(g.glyph_object.name)
# pass
if remove_me:
remove_list.append(i)
for i in remove_list:
if type(abc3d_data.available_texts[i].text_object) != type(None):
mom = abc3d_data.available_texts[i].text_object
def delif(o, p):
if p in o:
del o[p]
delif(mom, f"{utils.prefix()}_linked_textobject")
delif(mom, f"{utils.prefix()}_font_name")
delif(mom, f"{utils.prefix()}_face_name")
delif(mom, f"{utils.prefix()}_font_size")
delif(mom, f"{utils.prefix()}_letter_spacing")
delif(mom, f"{utils.prefix()}_orientation")
delif(mom, f"{utils.prefix()}_translation")
delif(mom, f"{utils.prefix()}_offset")
abc3d_data.available_texts.remove(i)
for i, t in enumerate(abc3d_data.available_texts):
if context.active_object == t.text_object:
active_text_index = i
if (hasattr(context.active_object, "parent") and
context.active_object.parent == t.text_object):
active_text_index = i
if active_text_index != abc3d_data.active_text_index:
abc3d_data.active_text_index = active_text_index
# butils.run_in_main_thread(update)
return True
def draw(self, context):
layout = self.layout
wm = context.window_manager
scene = context.scene
abc3d_data = scene.abc3d_data
layout.label(text="Text Objects")
layout.template_list("ABC3D_UL_texts", "", abc3d_data,
"available_texts", abc3d_data, "active_text_index")
layout.row().operator(
f"{__name__}.remove_text", text="Remove Textobject")
class ABC3D_PT_FontCreation(bpy.types.Panel):
bl_label = "Font Creation"
bl_parent_id = "ABC3D_PT_Panel"
bl_category = "ABC3D"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_options = {"DEFAULT_CLOSED"}
def draw(self, context):
layout = self.layout
wm = context.window_manager
scene = context.scene
abc3d_data = scene.abc3d_data
layout.row().operator(
f"{__name__}.create_font_from_objects", text='Create/Extend Font')
box = layout.box()
box.row().label(text="Exporting a fontfile")
box.row().label(text="1. Select export directory:")
box.prop(abc3d_data, 'export_dir')
box.row().label(text="2. More options and export:")
box.row().operator(f"{__name__}.save_font_to_file",
text='Export Font To File')
layout.row().operator(
f"{__name__}.toggle_abc3d_collection", text='Toggle Collection')
box = layout.box()
box.label(text="metrics")
box.row().operator(
f"{__name__}.add_default_metrics", text='Add Default Metrics')
box.row().operator(f"{__name__}.remove_metrics", text='Remove Metrics')
box.row().operator(f"{__name__}.align_metrics", text='Align Metrics')
box.row().operator(
f"{__name__}.align_metrics_to_active_object", text='Align Metrics to Active Object')
layout.row().operator(
f"{__name__}.temporaryhelper", text='Debug Function Do Not Use')
class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel):
bl_label = "Text Properties"
bl_parent_id = "ABC3D_PT_TextManagement"
bl_category = "ABC3D"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
def get_active_text_properties(self):
# and bpy.context.object.select_get():
if type(bpy.context.active_object) != type(None):
for t in bpy.context.scene.abc3d_data.available_texts:
if bpy.context.active_object == t.text_object:
return t
if bpy.context.active_object.parent == t.text_object:
return t
return None
# def font_items_callback(self, context):
# items = []
# fonts = Font.get_loaded_fonts_and_faces()
# for f in fonts:
# items.append((f"{f[0]} {f[1]}", f"{f[0]} {f[1]}", ""))
# return items
# def font_default_callback(self, context):
# t = self.get_active_text_properties(self)
# if type(t) != type(None):
# return f"{t.font_name} {t.face_name}"
# else:
# return None
# def font_update_callback(self, context):
# font_name, face_name = self.font.split(" ")
# t = self.get_active_text_properties(self)
# t.font_name = font_name
# t.face_name = face_name
# butils.set_text_on_curve(t)
# font: bpy.props.EnumProperty(
# items=font_items_callback,
# default=font_default_callback,
# update=font_update_callback,
# )
@classmethod
def poll(self, context):
return type(self.get_active_text_properties(self)) != type(None)
def draw(self, context):
layout = self.layout
wm = context.window_manager
scene = context.scene
abc3d_data = scene.abc3d_data
props = self.get_active_text_properties()
if type(props) == type(None) or type(props.text_object) == type(None):
# this should not happen
# as then polling does not work
# however, we are paranoid
return
layout.label(text=f"Mom: {props.text_object.name}")
layout.row().prop(props, "font")
layout.row().prop(props, "text")
layout.row().prop(props, "letter_spacing")
layout.row().prop(props, "font_size")
layout.row().prop(props, "offset")
layout.row().prop(props, "compensate_curvature")
layout.row().prop(props, "ignore_orientation")
layout.column().prop(props, "translation")
layout.column().prop(props, "orientation")
class ABC3D_OT_InstallFont(bpy.types.Operator):
"""Install or load Fontfile from path above.
(Format must be *.glb or *.gltf)"""
bl_idname = f"{__name__}.install_font"
bl_label = "Load Font"
bl_options = {'REGISTER', 'UNDO'}
def font_path_update_callback(self, context):
if os.path.exists(self.font_path):
print(f"{self.font_path} does exist")
else:
print(f"{self.font_path} does not exist")
font_path: bpy.props.StringProperty(
name="Font path",
description="Install a *.glb or *.gltf fontfile from disk",
default="",
maxlen=1024,
update=font_path_update_callback,
subtype="FILE_PATH")
install_in_assets: bpy.props.BoolProperty(
name="install in assets",
description="install the font in the assets directory of the addon",
default=True,
)
load_into_memory: bpy.props.BoolProperty(name="load font data into memory",
description="if false, it will load font data on demand",
default=False)
def draw(self, context):
abc3d_data = context.scene.abc3d_data
layout = self.layout
layout.row().prop(self, "font_path")
# layout.row().prop(abc3d_data, "font_path") # closes the stupid panel on Mac OS..
layout.row().prop(self, "install_in_assets")
if not self.install_in_assets and not self.load_into_memory:
layout.label(text="If the fontfile is not installed,")
layout.label(
text="and the font is not loaded in memory completely,")
layout.label(text="the fontfile should not be moved.")
layout.row().prop(self, "load_into_memory")
if self.load_into_memory:
layout.label(text="Loading font files can take a long time")
layout.label(text="and use a lot of RAM.")
layout.label(text="We recommend not doing this and let us")
layout.label(text="load the font data on demand.")
def invoke(self, context, event):
# self.font_path = butils.bpy_to_abspath(self.font_path)
# if not os.path.exists(self.font_path):
# bpy.app.timers.register(lambda: butils.ShowMessageBox(
# title=f"{__name__} Warning",
# icon="ERROR",
# message=[
# f"We believe the font path ({self.font_path}) does not exist.",
# f"Did you select your fontfile in the field above the 'Install new font'-button?",
# ],
# ), first_interval=0.1)
return context.window_manager.invoke_props_dialog(self)
def execute(self, context):
scene = bpy.context.scene
abc3d_data = context.scene.abc3d_data
font_path = butils.bpy_to_abspath(self.font_path)
if not os.path.exists(font_path):
butils.ShowMessageBox(
title=f"{__name__} Warning",
icon="ERROR",
message=[
f"Could not install font.",
f"We believe the font path ({font_path}) does not exist.",
f"If this is an error, please let us know.",
],
)
return {'CANCELLED'}
if self.install_in_assets:
preferences = getPreferences(context)
filename = os.path.basename(font_path)
target = os.path.join(preferences.assets_dir, "fonts", filename)
import shutil
os.makedirs(os.path.dirname(target), exist_ok=True)
shutil.copyfile(font_path, target)
# def register_load(target, load=False):
# print(f"registering installed fonts")
# bpy.app.timers.register(lambda: register_load(target, self.load_into_memory), first_interval=5)
butils.register_font_from_filepath(target)
if self.load_into_memory:
butils.load_font_from_filepath(target)
butils.update_available_fonts()
else:
butils.register_font_from_filepath(font_path)
if self.load_into_memory:
butils.load_font_from_filepath(font_path)
return {'FINISHED'}
class ABC3D_OT_OpenAssetDirectory(bpy.types.Operator):
"""Open Asset Directory"""
bl_idname = f"{__name__}.open_asset_directory"
bl_label = "Opens asset directory."
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
preferences = getPreferences(context)
directory = os.path.realpath(preferences.assets_dir)
if os.path.exists(directory):
utils.open_file_browser(directory)
return {'FINISHED'}
else:
butils.ShowMessageBox(
title=f"{__name__} Warning",
icon="ERROR",
message=("Asset directory does not exist.",
f"Command failed trying to access '{directory}'.",
"Please, make sure it exists or chose another directory."))
return {'CANCELLED'}
class ABC3D_OT_LoadInstalledFonts(bpy.types.Operator):
"""Load installed fontfiles from datapath."""
bl_idname = f"{__name__}.load_installed_fonts"
bl_label = "Loading installed Fonts."
bl_options = {'REGISTER', 'UNDO'}
load_into_memory: bpy.props.BoolProperty(name="load font data into memory",
description="if false, it will load font data on demand",
default=False)
def draw(self, context):
layout = self.layout
layout.row().prop(self, "load_into_memory")
if self.load_into_memory:
layout.label(text="Loading font files can take a long time")
layout.label(text="and use a lot of RAM.")
layout.label(text="We recommend not doing this and let us")
layout.label(text="load the font data on demand.")
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def execute(self, context):
print("EXECUTE LOAD INSTALLED FONTS")
scene = bpy.context.scene
if self.load_into_memory:
butils.load_installed_fonts()
else:
butils.register_installed_fonts()
butils.ShowMessageBox("Loading Fonts",
'INFO',
"Updating Data Structures.")
butils.update_available_fonts()
butils.ShowMessageBox("Loading Fonts",
'INFO',
"Done loading installed fonts.")
return {'FINISHED'}
class ABC3D_OT_LoadFont(bpy.types.Operator):
"""Load all glyphs from a specific font in memory.\nThis can take a while and slow down Blender."""
bl_idname = f"{__name__}.load_font"
bl_label = "Loading Font."
bl_options = {'REGISTER', 'UNDO'}
font_name: bpy.props.StringProperty()
face_name: bpy.props.StringProperty()
def execute(self, context):
filepaths = Font.fonts[self.font_name].faces[self.face_name].filepaths
for f in filepaths:
butils.load_font_from_filepath(f)
return {'FINISHED'}
class ABC3D_OT_AddDefaultMetrics(bpy.types.Operator):
"""Add default metrics to selected objects"""
bl_idname = f"{__name__}.add_default_metrics"
bl_label = "Add default metrics"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
objects = bpy.context.selected_objects
butils.add_default_metrics_to_objects(objects)
return {'FINISHED'}
class ABC3D_OT_RemoveMetrics(bpy.types.Operator):
"""Remove metrics from selected objects"""
bl_idname = f"{__name__}.remove_metrics"
bl_label = "Remove metrics"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
objects = bpy.context.selected_objects
butils.remove_metrics_from_objects(objects)
return {'FINISHED'}
class ABC3D_OT_AlignMetricsToActiveObject(bpy.types.Operator):
"""Align metrics of selected objects to metrics of active object"""
bl_idname = f"{__name__}.align_metrics_to_active_object"
bl_label = "Align metrics to active object"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
objects = bpy.context.selected_objects
butils.align_metrics_of_objects_to_active_object(objects)
return {'FINISHED'}
class ABC3D_OT_AlignMetrics(bpy.types.Operator):
"""Align metrics of selected objects to each other"""
bl_idname = f"{__name__}.align_metrics"
bl_label = "Align metrics"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
objects = bpy.context.selected_objects
butils.align_metrics_of_objects(objects)
return {'FINISHED'}
class ABC3D_OT_TemporaryHelper(bpy.types.Operator):
"""Temporary Helper ABC3D\nThis could do anything.\nIt's just there to make random functions available for testing."""
bl_idname = f"{__name__}.temporaryhelper"
bl_label = "Temp Font"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
global shared
scene = bpy.context.scene
abc3d_data = scene.abc3d_data
# butils.load_font_from_filepath("/home/jrkb/.config/blender/4.1/datafiles/abc3d/fonts/NM_Origin.glb")
butils.update_available_fonts()
# objects = bpy.context.selected_objects
# butils.add_default_metrics_to_objects(objects)
# reference_bound_box = None
# for o in objects:
# bb = o.bound_box
# reference_bound_box = butils.get_max_bound_box(bb, reference_bound_box)
# for o in objects:
# metrics = butils.get_metrics_bound_box(o.bound_box, reference_bound_box)
# butils.add_metrics_obj_from_bound_box(o, metrics)
# bpy.app.timers.register(lambda: butils.remove_metrics_from_objects(objects), first_interval=5)
return {'FINISHED'}
class ABC3D_OT_RemoveText(bpy.types.Operator):
"""Remove Text 3D"""
bl_idname = f"{__name__}.remove_text"
bl_label = "Remove Text"
bl_options = {'REGISTER', 'UNDO'}
remove_objects: bpy.props.BoolProperty(
name="Remove Objects",
description="Remove both ABC3D text functionality and the objects/meshes",
default=True)
def invoke(self, context, event):
wm = context.window_manager
return wm.invoke_props_dialog(self)
def execute(self, context):
abc3d_data = context.scene.abc3d_data
if abc3d_data.active_text_index < 0:
butils.ShowMessageBox(
title="No text selected",
message=("Please select a text."),
icon='GHOST_ENABLED')
return {'CANCELLED'}
i = abc3d_data.active_text_index
if type(abc3d_data.available_texts[i].text_object) != type(None):
mom = abc3d_data.available_texts[i].text_object
def delif(o, p):
if p in o:
del o[p]
delif(mom, f"{utils.prefix()}_linked_textobject")
delif(mom, f"{utils.prefix()}_font_name")
delif(mom, f"{utils.prefix()}_face_name")
delif(mom, f"{utils.prefix()}_font_size")
delif(mom, f"{utils.prefix()}_letter_spacing")
delif(mom, f"{utils.prefix()}_orientation")
delif(mom, f"{utils.prefix()}_translation")
delif(mom, f"{utils.prefix()}_offset")
if self.remove_objects:
remove_list = []
for g in abc3d_data.available_texts[i].glyphs:
if type(g) != type(None):
remove_list.append(g.glyph_object)
butils.simply_delete_objects(remove_list)
abc3d_data.available_texts.remove(i)
return {'FINISHED'}
class ABC3D_OT_PlaceText(bpy.types.Operator):
"""Place Text 3D on active object"""
bl_idname = f"{__name__}.placetext"
bl_label = "Place Text"
bl_options = {'REGISTER', 'UNDO'}
def font_items_callback(self, context):
items = []
fonts = Font.get_loaded_fonts_and_faces()
for f in fonts:
items.append((f"{f[0]} {f[1]}", f"{f[0]} {f[1]}", ""))
return items
def font_update_callback(self, context):
font_name, face_name = self.font.split(" ")
self.font_name = font_name
self.face_name = face_name
font_name: bpy.props.StringProperty(
options={'HIDDEN'}
)
face_name: bpy.props.StringProperty(
options={'HIDDEN'}
)
font: bpy.props.EnumProperty(items=font_items_callback,
update=font_update_callback
)
text: bpy.props.StringProperty(
name="Text",
description="The text.",
default="ABC3D",
maxlen=1024,
)
# target_object: bpy.props.PointerProperty(
# name="The Target Object",
# description="The target, which will be populated by character children of text.",
# type=bpy.types.Object,
# )
letter_spacing: bpy.props.FloatProperty(
name="Letter Spacing",
description="Letter Spacing",
default=0.0,
)
font_size: bpy.props.FloatProperty(
name="Font Size",
default=1.0,
subtype='NONE',
)
offset: bpy.props.FloatProperty(
name="Offset",
default=0.0,
subtype='NONE',
)
translation: bpy.props.FloatVectorProperty(
name="Translation",
default=(0.0, 0.0, 0.0),
subtype='TRANSLATION',
)
orientation: bpy.props.FloatVectorProperty(
name="Orientation",
default=(1.5707963267948966, 0.0, 0.0), # 90 degrees in radians
subtype='EULER',
)
def invoke(self, context, event):
wm = context.window_manager
self.font_update_callback(context)
return wm.invoke_props_dialog(self)
def execute(self, context):
global shared
scene = bpy.context.scene
abc3d_data = scene.abc3d_data
selected = bpy.context.view_layer.objects.active
if selected:
# font = abc3d_data.available_fonts[abc3d_data.active_font_index]
# font_name = font.font_name
# face_name = font.face_name
distribution_type = 'DEFAULT'
text_id = 0
for i, tt in enumerate(abc3d_data.available_texts):
while text_id == tt.text_id:
text_id = text_id + 1
t = abc3d_data.available_texts.add()
# If you wish to set a value and not fire an update, set the id property.
# A property defined via bpy.props for example ob.prop is stored as ob["prop"] once set to non default.
t['text_id'] = text_id
# t['font'] = self.font # enums want to be set as attribute
t['font_name'] = self.font_name
t['face_name'] = self.face_name
t.text_object = selected
t['text'] = self.text
t['letter_spacing'] = self.letter_spacing
t['font_size'] = self.font_size
t['offset'] = self.offset
t['translation'] = self.translation
t['orientation'] = self.orientation
t['distribution_type'] = distribution_type
t.font = self.font # enums want to be set as attribute
# this also calls the update function
# so we don't need to prepare/set again
# no need for these:
# butils.prepare_text(t.font_name,
# t.face_name,
# t.text)
# or this:
# butils.set_text_on_curve(t)
# else:
butils.ShowMessageBox(
title="No object selected",
message=(
"Please select an object.",
"It will be used to put the type on.",
"Thank you :)"),
icon='GHOST_ENABLED')
return {'FINISHED'}
class ABC3D_OT_ToggleABC3DCollection(bpy.types.Operator):
"""Toggle ABC3D Collection"""
bl_idname = f"{__name__}.toggle_abc3d_collection"
bl_label = "Toggle Collection visibility"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
scene = context.scene
fontcollection = bpy.data.collections.get("ABC3D")
if fontcollection is None:
self.report(
{'INFO'}, f"{bl_info['name']}: There is no collection. Did you use or create any glyphs yet?")
elif scene.collection.children.find(fontcollection.name) < 0:
scene.collection.children.link(fontcollection)
self.report({'INFO'}, f"{bl_info['name']}: show collection")
else:
scene.collection.children.unlink(fontcollection)
self.report({'INFO'}, f"{bl_info['name']}: hide collection")
return {'FINISHED'}
class ABC3D_OT_SaveFontToFile(bpy.types.Operator):
"""Save font to file"""
bl_idname = f"{__name__}.save_font_to_file"
bl_label = "Save Font"
bl_options = {'REGISTER', 'UNDO'}
def invoke(self, context, event):
wm = context.window_manager
preferences = getPreferences(context)
abc3d_data = context.scene.abc3d_data
if abc3d_data.export_dir == "":
abc3d_data.export_dir = os.path.join(
preferences.assets_dir, "fonts")
return wm.invoke_props_dialog(self)
def draw(self, context):
abc3d_data = context.scene.abc3d_data
layout = self.layout
layout.label(text="Available Fonts")
layout.template_list("ABC3D_UL_fonts", "", abc3d_data,
"available_fonts", abc3d_data, "active_font_index")
available_font = abc3d_data.available_fonts[abc3d_data.active_font_index]
font_name = available_font.font_name
face_name = available_font.face_name
loaded_glyphs = sorted(
Font.fonts[font_name].faces[face_name].loaded_glyphs)
n = 16
n_rows = int(len(loaded_glyphs) / n)
box = layout.box()
box.row().label(text=f"Glyphs to be exported:")
subbox = box.box()
for i in range(0, n_rows + 1):
text = ''.join([f"{u}" for ui, u in enumerate(
loaded_glyphs) if ui < (i+1) * n and ui >= i * n])
scale_y = 0.5
row = subbox.row()
row.scale_y = scale_y
row.label(text=text)
layout.prop(abc3d_data, 'export_dir')
def execute(self, context):
global shared
scene = bpy.context.scene
abc3d_data = scene.abc3d_data
fontcollection = bpy.data.collections.get("ABC3D")
# check if all is good to proceed
if fontcollection is None:
self.report({'INFO'}, f"{bl_info['name']}: There is no collection")
return {'CANCELLED'}
if abc3d_data.active_font_index < 0:
self.report(
{'INFO'}, f"{bl_info['name']}: There is no active font")
return {'CANCELLED'}
if len(abc3d_data.available_fonts) <= abc3d_data.active_font_index:
self.report(
{'INFO'}, f"{bl_info['name']}: Active font is not available")
return {'CANCELLED'}
# save state to restore later
was_fontcollection_linked = scene.collection.children.find(
fontcollection.name) >= 0
was_selection = []
for obj in bpy.context.selected_objects:
was_selection.append(obj)
was_active_object = bpy.context.view_layer.objects.active
bpy.ops.object.select_all(action="DESELECT")
# get save data
selected_font = abc3d_data.available_fonts[abc3d_data.active_font_index]
# print(selected_font.font_name)
self.report(
{'INFO'}, f"{bl_info['name']}: {selected_font.font_name} {selected_font.face_name}")
preferences = getPreferences(context)
print(f"assets folder: {preferences.assets_dir}")
bpy.ops.scene.new(type='FULL_COPY')
linked_collections = bpy.context.scene.collection.children.values()
for c in linked_collections:
bpy.context.scene.collection.children.unlink(c)
bpy.context.scene.collection.children.link(fontcollection)
# select what needs to be selected
export_objects = []
for obj in fontcollection.objects:
if obj["font_name"] == selected_font.font_name:
if not butils.is_metrics_object(obj):
obj.select_set(True)
export_objects.append(obj)
else:
obj.select_set(True)
butils.add_faces_to_metrics(obj)
export_objects.append(obj)
context_override = bpy.context.copy()
context_override["selected_objects"] = list(export_objects)
# context_override["scene"] = bpy.context.scene.copy()
with bpy.context.temp_override(**context_override):
filepath = f"{abc3d_data.export_dir}/{selected_font.font_name}.glb"
# get rid of scene extra data before export
scene_keys = []
for k in bpy.context.scene.keys():
scene_keys.append(k)
for k in scene_keys:
del bpy.context.scene[k]
# save as gltf
bpy.ops.export_scene.gltf(
filepath=filepath,
check_existing=False,
# GLB or GLTF_SEPARATE (also change filepath)
export_format='GLB',
export_extras=True,
use_selection=True,
use_active_scene=True,
)
bpy.app.timers.register(
lambda: bpy.ops.scene.delete(), first_interval=1)
# bpy.ops.scene.delete()
# restore()
def remove_faces():
for obj in fontcollection.objects:
if obj["font_name"] == selected_font.font_name:
if butils.is_metrics_object(obj):
butils.remove_faces_from_metrics(obj)
bpy.app.timers.register(lambda: remove_faces(), first_interval=2)
self.report({'INFO'}, f"did it")
return {'FINISHED'}
# keep = ['io_anim_bvh', 'io_curve_svg', 'io_mesh_stl', 'io_mesh_uv_layout', 'io_scene_fbx', 'io_scene_gltf2', 'io_scene_x3d', 'cycles', 'pose_library', 'abc3d']
# for addon in keep:
# bpy.ops.preferences.addon_enable(module=addon)
class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator):
"""Create Font from selected objects"""
bl_idname = f"{__name__}.create_font_from_objects"
bl_label = "Create Font"
bl_options = {'REGISTER', 'UNDO'}
font_name: bpy.props.StringProperty(
default="NM_Origin",
)
face_name: bpy.props.StringProperty(
default="Tender",
)
import_infix: bpy.props.StringProperty(
default="_NM_Origin_Tender",
)
autodetect_names: bpy.props.BoolProperty(
default=True,
)
fix_common_misspellings: bpy.props.BoolProperty(
default=True,
)
def invoke(self, context, event):
wm = context.window_manager
return wm.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
if len(context.selected_objects) == 0:
layout.row().label(text="No objects selected.", icon="ERROR")
layout.row().label(text="Please select your glyphs first.", icon="INFO")
else:
row = layout.row()
row.prop(self, 'autodetect_names')
if self.autodetect_names:
scale_y = 0.5
row = layout.row()
row.scale_y = scale_y
row.label(
text="Watch out, follow convention in naming your meshes:")
row = layout.row()
row.scale_y = scale_y
row.label(text="'<glyph id>_<font name>_<face name>'")
row = layout.row()
row.scale_y = scale_y
row.label(text=" - glyph id: unicode glyph name or raw glyph")
row = layout.row()
row.scale_y = scale_y
row.label(text=" - font name: font name with underscore")
row = layout.row()
row.scale_y = scale_y
row.label(text=" - face name: face name")
row = layout.row()
row.scale_y = scale_y
row.label(text="working examples:")
row = layout.row()
row.scale_y = scale_y
row.label(text="- 'A_NM_Origin_Tender'")
row = layout.row()
row.scale_y = scale_y
row.label(text="- 'B_NM_Origin_Tender'")
row = layout.row()
row.scale_y = scale_y
row.label(text="- 'arrowright_NM_Origin_Tender'")
row = layout.row()
row.scale_y = scale_y
row.label(text="- '→_NM_Origin_Tender' (equal to above)")
row = layout.row()
row.scale_y = scale_y
row.label(text="- 'quotesingle_NM_Origin_Tender.001'")
row = layout.row()
row.scale_y = scale_y
row.label(text="- 'colon_NM_Origin_Tender_2'")
box = layout.box()
box.enabled = not self.autodetect_names
box.prop(self, 'font_name')
box.prop(self, 'face_name')
box.prop(self, 'import_infix')
layout.prop(self, 'fix_common_misspellings')
if self.fix_common_misspellings:
for k in Font.known_misspellings:
character = ""
if Font.known_misspellings[k] in Font.name_to_glyph_d:
character = f" ({Font.name_to_glyph_d[Font.known_misspellings[k]]})"
row = layout.row()
row.scale_y = 0.5
row.label(
text=f"{k}{Font.known_misspellings[k]}{character}")
def execute(self, context):
print(f"executing {self.bl_idname}")
if len(context.selected_objects) == 0:
print(f"cancelled {self.bl_idname} - no objects selected")
return {'CANCELLED'}
global shared
scene = bpy.context.scene
abc3d_data = scene.abc3d_data
fontcollection = bpy.data.collections.get("ABC3D")
if fontcollection is None:
fontcollection = bpy.data.collections.new("ABC3D")
ifxsplit = self.import_infix.split('_')
# if len(ifxsplit) != 4:
# font_name = f"{ifxsplit[1]}_{ifxsplit[2]}"
# face_name = ifxsplit[3]
font_name = self.font_name
face_name = self.face_name
# TODO: do not clear
# abc3d_data.available_fonts.clear()
# Font.fonts = {}
currentObjects = []
for o in context.selected_objects:
if o.name not in currentObjects:
print(f"processing {o.name}")
process_object = True
if self.autodetect_names:
ifxsplit = o.name.split('_')
if len(ifxsplit) < 4:
print(
f"whoops name could not be autodetected {o.name}")
continue
font_name = f"{ifxsplit[1]}_{ifxsplit[2]}"
face_name = ifxsplit[3]
if butils.is_mesh(o) and not butils.is_metrics_object(o):
uc = o.users_collection
if self.fix_common_misspellings:
o.name = Font.fix_glyph_name_misspellings(o.name)
# name = re.sub(regex, "", o.name)
# glyph_id = Font.name_to_glyph(name)
name = o.name.split('_')[0]
glyph_id = Font.name_to_glyph(name)
if type(glyph_id) != type(None):
o["glyph"] = glyph_id
o["font_name"] = font_name
o["face_name"] = face_name
# butils.apply_all_transforms(o)
butils.move_in_fontcollection(
o,
fontcollection)
Font.add_glyph(
font_name,
face_name,
glyph_id,
o)
# TODO: is there a better way to iterate over a CollectionProperty?
found = False
for f in abc3d_data.available_fonts.values():
if (f.font_name == font_name
and f.face_name == face_name):
found = True
break
if not found:
f = abc3d_data.available_fonts.add()
f.font_name = font_name
f.face_name = face_name
else:
print(
f"import warning: did not understand glyph {name}")
self.report(
{'INFO'}, f"did not understand glyph {name}")
return {'FINISHED'}
class ABC3D_PT_RightPropertiesPanel(bpy.types.Panel):
"""Creates a Panel in the Object properties window"""
bl_label = f"{bl_info['name']}"
bl_idname = "ABC3D_PT_RightPropertiesPanel"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "object"
@classmethod
def poll(self, context):
# only show the panel, if it's a textobject or a glyph
is_text = type(next((t for t in context.scene.abc3d_data.available_texts if t.text_object ==
context.active_object), None)) != type(None)
is_glyph = type(next((t for t in context.scene.abc3d_data.available_texts if t.text_object ==
context.active_object.parent), None)) != type(None)
return is_text or is_glyph
def draw(self, context):
layout = self.layout
scene = context.scene
abc3d_data = scene.abc3d_data
obj = context.active_object
def is_it_text():
return type(next((t for t in context.scene.abc3d_data.available_texts if t.text_object == context.active_object), None)) != type(None)
def is_it_glyph():
return type(next((t for t in context.scene.abc3d_data.available_texts if t.text_object == context.active_object.parent), None)) != type(None)
is_text = is_it_text()
is_glyph = is_it_glyph()
textobject = obj if is_text else obj.parent if is_glyph else obj
available_text = abc3d_data.available_texts[abc3d_data.active_text_index]
# row = layout.row()
# row.label(text="Hello world!", icon='WORLD_DATA')
# row = layout.row()
# row.label(text="Active object is: " + obj.name)
# row = layout.row()
# row.label(text="text object is: " + textobject.name)
row = layout.row()
row.label(text=f"active text index is: {abc3d_data.active_text_index}")
layout.row().label(text="Text Properties:")
layout.row().prop(available_text, "text")
layout.row().prop(available_text, "letter_spacing")
layout.row().prop(available_text, "font_size")
layout.row().prop(available_text, "offset")
layout.row().prop(available_text, "compensate_curvature")
layout.row().prop(available_text, "ignore_orientation")
layout.column().prop(available_text, "translation")
layout.column().prop(available_text, "orientation")
if is_glyph:
layout.row().label(text="Glyph Properties:")
class ABC3D_OT_Reporter(bpy.types.Operator):
bl_idname = f"{__name__}.reporter"
bl_label = "Report"
label = bpy.props.StringProperty(
name="label",
default="INFO",
)
message = bpy.props.StringProperty(
name="message",
default="I have nothing to say really",
)
def execute(self, context):
# this is where I send the message
self.report({'INFO'}, 'whatever')
for i in range(0, 10):
butils.ShowMessageBox('whatever', 'INFO', 'INFO')
return {'FINISHED'}
classes = (
bimport.ImportGLTF2,
bimport.GetFontFacesInFile,
ABC3D_addonPreferences,
ABC3D_available_font,
ABC3D_glyph_properties,
ABC3D_text_properties,
ABC3D_data,
ABC3D_UL_fonts,
ABC3D_UL_texts,
ABC3D_PT_Panel,
# ABC3D_PT_LoadFontPanel,
ABC3D_PT_FontList,
ABC3D_PT_TextPlacement,
ABC3D_PT_TextManagement,
ABC3D_PT_FontCreation,
ABC3D_PT_TextPropertiesPanel,
ABC3D_OT_OpenAssetDirectory,
ABC3D_OT_LoadInstalledFonts,
ABC3D_OT_LoadFont,
ABC3D_OT_AddDefaultMetrics,
ABC3D_OT_RemoveMetrics,
ABC3D_OT_AlignMetricsToActiveObject,
ABC3D_OT_AlignMetrics,
ABC3D_OT_TemporaryHelper,
ABC3D_OT_RemoveText,
ABC3D_OT_PlaceText,
ABC3D_OT_InstallFont,
ABC3D_OT_ToggleABC3DCollection,
ABC3D_OT_SaveFontToFile,
ABC3D_OT_CreateFontFromObjects,
ABC3D_PT_RightPropertiesPanel,
ABC3D_OT_Reporter,
)
def compare_text_object_with_object(t, o, strict=False):
for k in o.keys():
if k == f"{utils.prefix()}_type":
if o[k] != "textobject":
return False
elif k.startswith(f"{utils.prefix()}_"):
p = k.replace(f"{utils.prefix()}_", "")
if p in t.keys():
if t[p] != o[k]:
return False
else:
print(f"{__name__} set_text_object: did not find key ({p})")
if strict:
return False
# for p in t.keys():
# if
return True
def detect_text():
scene = bpy.context.scene
abc3d_data = scene.abc3d_data
for o in scene.objects:
if o[f"{utils.prefix()}_type"] == "textobject":
linked_textobject = int(o[f"{utils.prefix()}_linked_textobject"])
if len(abc3d_data.available_texts) > linked_textobject \
and abc3d_data.available_texts[linked_textobject].text_object == o:
t = abc3d_data.available_texts[linked_textobject]
a = test_availability(
o["font_name"], o["face_name"], o["text"])
butils.transfer_blender_object_to_text_properties(o, t)
def load_used_glyphs():
print("LOAD USED GLYPHS")
scene = bpy.context.scene
abc3d_data = scene.abc3d_data
for t in abc3d_data.available_texts:
a = Font.test_availability(t.font_name,
t.face_name,
t.text)
if type(a) == type(int()):
if a == Font.MISSING_FONT:
butils.ShowMessageBox("Missing Font",
"ERROR",
[f"Font {t.font_name} is missing.",
"Do you have it installed?"])
if a is Font.MISSING_FACE:
butils.ShowMessageBox("Missing FontFace",
"ERROR",
[f"Font {t.font_name} is there,",
f"but the FontFace {t.face_name} is missing,",
"Do you have it installed?"])
elif len(a["maybe"]) > 0:
for fp in a["filepaths"]:
butils.load_font_from_filepath(fp, a["maybe"])
@persistent
def load_handler(self, dummy):
if not bpy.app.timers.is_registered(butils.execute_queued_functions):
bpy.app.timers.register(butils.execute_queued_functions)
butils.run_in_main_thread(butils.update_available_fonts)
butils.run_in_main_thread(bpy.ops.abc3d.load_installed_fonts)
butils.run_in_main_thread(load_used_glyphs)
def load_handler_unload():
if bpy.app.timers.is_registered(butils.execute_queued_functions):
bpy.app.timers.unregister(butils.execute_queued_functions)
@persistent
def on_frame_changed(self, dummy):
for t in bpy.context.scene.abc3d_data.available_texts:
# TODO PERFORMANCE: only on demand
butils.set_text_on_curve(t)
@persistent
def on_depsgraph_update(scene, depsgraph):
if not bpy.context.mode.startswith("EDIT"):
for u in depsgraph.updates:
if f"{utils.prefix()}_linked_textobject" in u.id.keys() \
and f"{utils.prefix()}_type" in u.id.keys() \
and u.id[f"{utils.prefix()}_type"] == 'textobject':
linked_textobject = u.id[f"{utils.prefix()}_linked_textobject"]
if u.is_updated_geometry and len(scene.abc3d_data.available_texts) > linked_textobject:
def later():
if not "lock_depsgraph_update_ntimes" in scene.abc3d_data \
or scene.abc3d_data["lock_depsgraph_update_ntimes"] == 0:
print("******* not yet")
butils.set_text_on_curve(
scene.abc3d_data.available_texts[linked_textobject])
elif scene.abc3d_data["lock_depsgraph_update_ntimes"] > 0:
scene.abc3d_data['lock_depsgraph_update_ntimes'] -= 1
butils.run_in_main_thread(later)
def register():
addon_updater_ops.register(bl_info)
for cls in classes:
addon_updater_ops.make_annotations(cls) # Avoid blender 2.8 warnings.
bpy.utils.register_class(cls)
bpy.types.Scene.abc3d_data = bpy.props.PointerProperty(type=ABC3D_data)
# 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
if load_handler not in bpy.app.handlers.load_post:
bpy.app.handlers.load_post.append(load_handler)
# and autostart if we reload script
load_handler(None, None)
if on_frame_changed not in bpy.app.handlers.frame_change_post:
bpy.app.handlers.frame_change_post.append(on_frame_changed)
if on_depsgraph_update not in bpy.app.handlers.depsgraph_update_post:
bpy.app.handlers.depsgraph_update_post.append(on_depsgraph_update)
butils.run_in_main_thread(butils.clear_available_fonts)
# butils.run_in_main_thread(butils.load_installed_fonts)
butils.run_in_main_thread(butils.update_available_fonts)
butils.run_in_main_thread(butils.update_types)
# bpy.ops.abc3d.load_installed_fonts()
Font.name_to_glyph_d = Font.generate_name_to_glyph_d()
def unregister():
addon_updater_ops.unregister()
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
# remove autostart when loading blend file
if load_handler in bpy.app.handlers.load_post:
bpy.app.handlers.load_post.remove(load_handler)
# and when reload script
load_handler_unload()
if on_frame_changed in bpy.app.handlers.frame_change_post:
bpy.app.handlers.frame_change_post.remove(on_frame_changed)
del bpy.types.Scene.abc3d_data
print(f"UNREGISTER {bl_info['name']}")
if __name__ == '__main__':
register()