font3d_blender_addon/__init__.py
2024-08-28 17:45:27 +02:00

1494 lines
57 KiB
Python

# SPDX-License-Identifier: GPL-2.0-only
"""
A 3D font helper
"""
bl_info = {
"name": "ABC3D",
"author": "Jakob Schlötter, Studio Pointer*",
"version": (0, 0, 1),
"blender": (4, 1, 0),
"location": "VIEW3D",
"description": "Does ABC3D stuff",
"category": "Typography",
}
# make sure that modules are reloadable
# when registering
# handy for development
# first import dependencies for the method
import importlib
# then import dependencies for our addon
if "bpy" in locals():
importlib.reload(Font)
importlib.reload(utils)
importlib.reload(butils)
importlib.reload(bimport)
else:
from .common import Font
from .common import utils
from . import butils
from . import bimport
import bpy
import math
import mathutils
import io
import functools
from bpy.types import Panel
from bpy.app.handlers import persistent
from random import uniform
import time
import datetime
import os
import re
def getPreferences(context):
preferences = context.preferences
return preferences.addons[__name__].preferences
class ABC3D_addonPreferences(bpy.types.AddonPreferences):
"""ABC3D Addon Preferences
These are the preferences at Edit/Preferences/Add-ons"""
bl_idname = __name__
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")
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):
layout.label(text=f"{index}: {item.font_name} {item.face_name}") # avoids renaming the item by accident
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))
split.label(text=f"{item.text}") # avoids renaming the item by accident
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__}.load_installed_fonts", text="load installed fonts", icon=icon)
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):
if type(bpy.context.active_object) != type(None):# and bpy.context.object.select_get():
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") # crashes on Mac OS?
# 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):
abc3d_data = context.scene.abc3d_data
self.font_path = abc3d_data.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
if not os.path.exists(self.font_path):
butils.ShowMessageBox(
title=f"{__name__} Warning",
icon="ERROR",
message=[
f"Could not install font.",
f"We believe the font path ({self.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(self.font_path)
target = os.path.join(preferences.assets_dir, "fonts", filename)
import shutil
os.makedirs(os.path.dirname(target), exist_ok=True)
shutil.copyfile(self.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(self.font_path)
if self.load_into_memory:
butils.load_font_from_filepath(self.font_path)
return {'FINISHED'}
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,
export_format='GLB', # GLB or GLTF_SEPARATE (also change filepath)
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_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 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)
def register():
for cls in classes:
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)
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)
# bpy.ops.abc3d.load_installed_fonts()
Font.name_to_glyph_d = Font.generate_name_to_glyph_d()
def unregister():
for cls in 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()