font3d_blender_addon/__init__.py

1723 lines
59 KiB
Python

# SPDX-License-Identifier: GPL-2.0-only
"""
A 3D font helper
"""
import importlib
import os
import bpy
from bpy.app.handlers import persistent
from . import addon_updater_ops, bimport, butils
from .common import Font, utils
bl_info = {
"name": "ABC3D",
"author": "Jakob Schlötter, Studio Pointer*",
"version": (0, 0, 5),
"blender": (4, 1, 0),
"location": "VIEW3D",
"description": "Convenience addon for 3D fonts",
"category": "Typography",
}
# NOTE: also change version in common/utils.py
# 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)
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")
# 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
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 not isinstance(self.font_name, None) and not isinstance(self.face_name, 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)
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="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_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="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="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_lf = row.operator(
f"{__name__}.load_font", text="Load all glyphs in memory"
)
oper_lf.font_name = font_name
oper_lf.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 not isinstance(c.get(f"{utils.prefix()}_linked_textobject"), 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__}.toggle_abc3d_collection", text="Toggle Collection"
)
layout.row().operator(
f"{__name__}.create_font_from_objects", text="Create/Extend Font"
)
layout.row().operator(
f"{__name__}.save_font_to_file", text="Export Font To File"
)
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):
font_path = butils.bpy_to_abspath(self.font_path)
if os.path.exists(font_path):
print(f"font_path_update: {font_path} does exist")
else:
print(f"font_path_update: {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=[
"Could not install font.",
f"We believe the font path ({font_path}) does not exist.",
"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.
The metrics of the active object are not changed and is taken as a reference for all other objects.
"""
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.
The metrics of all objects are merged and expanded to fit for all objects."""
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()}_type")
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.
This will show the Fonts and Glyphs currently loaded by ABC3D. Useful for font creation, debugging and inspection."""
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="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"}, "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",
)
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 do_autodetect_names(self, name: str):
ifxsplit = name.split("_")
if len(ifxsplit) < 4:
print(f"name could not be autodetected {name}")
print("split:")
print(ifxsplit)
return self.font_name, self.face_name
detected_font_name = f"{ifxsplit[1]}_{ifxsplit[2]}"
detected_face_name = ifxsplit[3]
return detected_font_name, detected_face_name
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")
first_object_name = context.selected_objects[-1].name
self.font_name, self.face_name = self.do_autodetect_names(first_object_name)
if self.autodetect_names:
scale_y = 0.5
row = layout.row()
row.scale_y = scale_y
row.label(text="Autodetecting names per glyph.")
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")
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")
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:
font_name, face_name = self.do_autodetect_names(o.name)
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, bpy.types.PointerProperty(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_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_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_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():
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)
depsgraph_updates_locked = False
def unlock_depsgraph_updates():
global depsgraph_updates_locked
depsgraph_updates_locked = False
def lock_depsgraph_updates():
global depsgraph_updates_locked
depsgraph_updates_locked = True
if bpy.app.timers.is_registered(unlock_depsgraph_updates):
bpy.app.timers.unregister(unlock_depsgraph_updates)
bpy.app.timers.register(unlock_depsgraph_updates, first_interval=1)
import time
@persistent
def on_depsgraph_update(scene, depsgraph):
global depsgraph_updates_locked
if not bpy.context.mode.startswith("EDIT") and not depsgraph_updates_locked:
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
):
lock_depsgraph_updates()
def later():
if (
"lock_depsgraph_update_ntimes" not in scene.abc3d_data
or scene.abc3d_data["lock_depsgraph_update_ntimes"] <= 0
):
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():
print(f"REGISTER {utils.prefix()}")
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}")
# autostart 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.init()
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)
if on_depsgraph_update in bpy.app.handlers.depsgraph_update_post:
bpy.app.handlers.depsgraph_update_post.remove(on_depsgraph_update)
del bpy.types.Scene.abc3d_data
print(f"UNREGISTER {utils.prefix()}")
if __name__ == "__main__":
register()