font3d_blender_addon/__init__.py
themancalledjakob 850377e0f5 add offset
2024-08-21 17:31:19 +02:00

1406 lines
52 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_settings(bpy.types.PropertyGroup):
font_path: bpy.props.StringProperty(
name="Font path",
description="Load a *.glb or *.gltf fontfile from disk",
default="",
maxlen=1024,
subtype="FILE_PATH")
import_infix: bpy.props.StringProperty(
name="Font name import infix",
description="The infix which all font objects to import have. obj name: 'A_NM_Origin_Tender' -> infix: '_NM_Origin_Tender'",
default="_NM_",
maxlen=1024,
)
text: bpy.props.StringProperty(
name="Text",
description="The text.",
default="HELLO",
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',
)
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',
)
offset: bpy.props.FloatProperty(
name="Offset",
default=0.0,
subtype='NONE',
)
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",
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")
active_font_index: bpy.props.IntProperty()
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)
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"
bl_options = {"DEFAULT_CLOSED"}
def draw(self, context):
layout = self.layout
wm = context.window_manager
scene = context.scene
abc3d = scene.abc3d
abc3d_data = scene.abc3d_data
layout.label(text="Install FontFile:")
layout.row().prop(abc3d, "font_path")
layout.row().operator(f"{__name__}.install_font", text='Install')
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 = scene.abc3d
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)
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 = scene.abc3d
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"
# TODO: perhaps this should be done in a periodic timer
@classmethod
def poll(self, context):
scene = context.scene
abc3d = scene.abc3d
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 = scene.abc3d
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 = scene.abc3d
abc3d_data = scene.abc3d_data
layout.row().operator(f"{__name__}.create_font_from_objects", text='Create/Extend Font')
layout.row().operator(f"{__name__}.save_font_to_file", text='Save 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 = scene.abc3d
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'}
install_in_assets: bpy.props.BoolProperty(
name="install in assets",
description="install the font in the assets directory of the addon",
)
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, "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):
return context.window_manager.invoke_props_dialog(self)
def execute(self, context):
scene = bpy.context.scene
if not os.path.exists(scene.abc3d.font_path):
butils.ShowMessageBox(
title=f"{__name__} Warning",
icon="ERROR",
message=f"We believe the font path ({scene.abc3d.font_path}) does not exist.",
)
return {'CANCELLED'}
if self.install_in_assets:
preferences = getPreferences(context)
filename = os.path.basename(scene.abc3d.font_path)
target = os.path.join(preferences.assets_dir, "fonts", filename)
print(f"installing {scene.abc3d.font_path} -> {target}")
import shutil
shutil.copyfile(scene.abc3d.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(scene.abc3d.font_path)
if self.load_into_memory:
butils.load_font_from_filepath(scene.abc3d.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):
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_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):
"""Temp Font 3D"""
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="HELLO",
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 = scene.abc3d
abc3d_data = scene.abc3d_data
selected = bpy.context.view_layer.objects.active
# if abc3d.target_object:
# selected = abc3d.target_object
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_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
butils.prepare_text(t.font_name,
t.face_name,
t.text)
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")
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'}
save_path: bpy.props.StringProperty(name="save_path", subtype="DIR_PATH")
def invoke(self, context, event):
wm = context.window_manager
return wm.invoke_props_dialog(self)
# def draw(self, contex):
# layout = self.layout
# layout.prop(self, 'font_name')
# layout.prop(self, 'face_name')
# layout.prop(self, 'import_infix')
# layout.prop(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]]})"
# layout.label(text=f"{k} -> {Font.known_misspellings[k]}{character}")
def execute(self, context):
global shared
scene = bpy.context.scene
abc3d_data = scene.abc3d_data
abc3d = scene.abc3d
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"{preferences.assets_dir}/fonts/{selected_font.font_name}.gltf"
# 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
export_extras=True,
# export_hierarchy_full_collections=True,
# use_active_collection_with_nested=True,
use_selection=True,
use_active_scene=True,
)
# bpy.app.timers.register(lambda: bpy.ops.scene.delete(), first_interval=5)
bpy.ops.scene.delete()
# restore()
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)
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, contex):
layout = self.layout
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}")
global shared
scene = bpy.context.scene
abc3d = scene.abc3d
abc3d_data = scene.abc3d_data
fontcollection = bpy.data.collections.get("ABC3D")
if fontcollection is None:
fontcollection = bpy.data.collections.new("ABC3D")
ifxsplit = abc3d.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
# regex = f"{abc3d.import_infix}(.)*"
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 = scene.abc3d
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')
print("lalala reporter")
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_settings,
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_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,
)
@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)
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 = bpy.props.PointerProperty(type=ABC3D_settings)
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
del bpy.types.Scene.abc3d_data
print(f"UNREGISTER {bl_info['name']}")
if __name__ == '__main__':
register()