font3d_blender_addon/__init__.py
themancalledjakob f3e58b2dab font loading
filter metrics
2024-08-08 11:30:39 +02:00

944 lines
34 KiB
Python

# SPDX-License-Identifier: GPL-2.0-only
"""
A 3D font helper
"""
bl_info = {
"name": "Font3D",
"author": "Jakob Schlötter, Studio Pointer*",
"version": (0, 0, 1),
"blender": (4, 1, 0),
"location": "VIEW3D",
"description": "Does Font3D 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)
else:
from .common import Font
from .common import utils
from . import butils
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 FONT3D_addonPreferences(bpy.types.AddonPreferences):
"""Font3D 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 FONT3D_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',
)
class FONT3D_available_font(bpy.types.PropertyGroup):
font_name: bpy.props.StringProperty(name="")
face_name: bpy.props.StringProperty(name="")
class FONT3D_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 FONT3D_text_properties(bpy.types.PropertyGroup):
def update_callback(self, context):
butils.set_text_on_curve(self)
text_id: bpy.props.IntProperty()
font_name: bpy.props.StringProperty()
font_face: bpy.props.StringProperty()
text_object: bpy.props.PointerProperty(type=bpy.types.Object)
text: bpy.props.StringProperty(
update=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',
)
compensate_curvature: bpy.props.BoolProperty(
update=update_callback,
name="Compensate Curvature",
default=True,
)
ignore_orientation: bpy.props.BoolProperty(
update=update_callback,
name="Ignore Orientation",
default=False,
)
distribution_type: bpy.props.StringProperty()
glyphs: bpy.props.CollectionProperty(type=FONT3D_glyph_properties)
#TODO: simply, merge, cut cut cut
class FONT3D_data(bpy.types.PropertyGroup):
available_fonts: bpy.props.CollectionProperty(type=FONT3D_available_font, name="name of the collection property")
active_font_index: bpy.props.IntProperty()
available_texts: bpy.props.CollectionProperty(type=FONT3D_text_properties, name="")
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 FONT3D_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 FONT3D_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 FONT3D_PT_Panel(bpy.types.Panel):
bl_label = f"{__name__} panel"
bl_category = "Font3D"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
def draw(self, context):
layout = self.layout
layout.label(text=f"{__name__} panel")
class FONT3D_PT_LoadFontPanel(bpy.types.Panel):
bl_label = "Load a new font"
bl_parent_id = "FONT3D_PT_Panel"
bl_category = "Font3D"
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
font3d = scene.font3d
font3d_data = scene.font3d_data
layout.label(text="Load FontFile:")
layout.row().prop(font3d, "font_path")
layout.row().operator('font3d.loadfont', text='Load Font')
class FONT3D_PT_FontList(bpy.types.Panel):
bl_label = "Font List"
bl_parent_id = "FONT3D_PT_Panel"
bl_category = "Font3D"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
def draw(self, context):
layout = self.layout
wm = context.window_manager
scene = context.scene
font3d = scene.font3d
font3d_data = scene.font3d_data
layout.label(text="Loaded Fonts")
layout.template_list("FONT3D_UL_fonts", "", font3d_data, "available_fonts", font3d_data, "active_font_index")
class FONT3D_PT_TextPlacement(bpy.types.Panel):
bl_label = "Place Text"
bl_parent_id = "FONT3D_PT_Panel"
bl_category = "Font3D"
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
font3d = scene.font3d
font3d_data = scene.font3d_data
layout.label(text="Set Properties Objects")
layout.row().prop(font3d, "text")
layout.row().prop(font3d, "letter_spacing")
layout.row().prop(font3d, "font_size")
layout.column().prop(font3d, "translation")
layout.column().prop(font3d, "orientation")
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 FONT3D_PT_TextManagement(bpy.types.Panel):
bl_label = "Text Management"
bl_parent_id = "FONT3D_PT_Panel"
bl_category = "Font3D"
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
font3d = scene.font3d
font3d_data = scene.font3d_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(font3d_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('linked_textobject')) != type(None) and c.get('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:
font3d_data.available_texts.remove(i)
for i, t in enumerate(font3d_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 != font3d_data.active_text_index:
font3d_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
font3d = scene.font3d
font3d_data = scene.font3d_data
layout.label(text="Text Objects")
layout.template_list("FONT3D_UL_texts", "", font3d_data, "available_texts", font3d_data, "active_text_index")
class FONT3D_PT_FontCreation(bpy.types.Panel):
bl_label = "Font Creation"
bl_parent_id = "FONT3D_PT_Panel"
bl_category = "Font3D"
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
font3d = scene.font3d
font3d_data = scene.font3d_data
layout.row().label(text="Font name import infix:")
layout.row().prop(font3d, "import_infix", text="")
layout.row().operator('font3d.create_font_from_objects', text='Create Font')
layout.row().operator('font3d.save_font_to_file', text='Save Font To File')
layout.row().operator('font3d.toggle_font3d_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('font3d.temporaryhelper', text='Debug Function Do Not Use')
class FONT3D_PT_TextPropertiesPanel(bpy.types.Panel):
bl_label = "Text Properties"
bl_parent_id = "FONT3D_PT_TextManagement"
bl_category = "Font3D"
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.font3d_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
@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
font3d = scene.font3d
font3d_data = scene.font3d_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, "text")
layout.row().prop(props, "letter_spacing")
layout.row().prop(props, "font_size")
layout.row().prop(props, "compensate_curvature")
layout.row().prop(props, "ignore_orientation")
layout.column().prop(props, "translation")
layout.column().prop(props, "orientation")
class FONT3D_OT_LoadFont(bpy.types.Operator):
"""Load Fontfile from path above.
(Format must be *.glb or *.gltf)"""
bl_idname = f"{__name__}.loadfont"
bl_label = "Load Font"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
scene = bpy.context.scene
butils.ShowMessageBox(
title=f"{__name__} Warning",
icon="ERROR",
message=f"We believe this functionality is currently not available.",
)
return {'CANCELLED'}
if not os.path.exists(scene.font3d.font_path):
butils.ShowMessageBox(
title=f"{__name__} Warning",
icon="ERROR",
message=f"We believe the font path ({scene[__name__].font_path}) does not exist.",
)
return {'CANCELLED'}
butils.load_font_from_filepath(scene.font3d.font_path)
return {'FINISHED'}
class FONT3D_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 FONT3D_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 FONT3D_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 FONT3D_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 FONT3D_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
font3d_data = scene.font3d_data
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 FONT3D_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 execute(self, context):
global shared
scene = bpy.context.scene
selected = bpy.context.view_layer.objects.active
font3d = scene.font3d
font3d_data = scene.font3d_data
if font3d.target_object:
selected = font3d.target_object
if selected:
font_name = "NM_Origin"
font_face = "Tender"
distribution_type = 'DEFAULT'
text_id = 0
for i, tt in enumerate(font3d_data.available_texts):
while text_id == tt.text_id:
text_id = text_id + 1
t = font3d_data.available_texts.add()
t.text_id = text_id
t.font_name = font_name
t.font_face = font_face
t.text_object = selected
t.text = scene.font3d.text
t.letter_spacing = scene.font3d.letter_spacing
t.font_size = scene.font3d.font_size
t.translation = scene.font3d.translation
t.orientation = scene.font3d.orientation
t.distribution_type = distribution_type
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 FONT3D_OT_ToggleFont3DCollection(bpy.types.Operator):
"""Toggle Font3D Collection"""
bl_idname = f"{__name__}.toggle_font3d_collection"
bl_label = "Toggle Collection visibility"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
scene = context.scene
fontcollection = bpy.data.collections.get("Font3D")
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 FONT3D_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 execute(self, context):
global shared
scene = bpy.context.scene
font3d_data = scene.font3d_data
font3d = scene.font3d
fontcollection = bpy.data.collections.get("Font3D")
# 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 font3d_data.active_font_index < 0:
self.report({'INFO'}, f"{bl_info['name']}: There is no active font")
return {'CANCELLED'}
if len(font3d_data.available_fonts) <= font3d_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 = font3d_data.available_fonts[font3d_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):
# butils.add_faces_to_metrics(obj)
obj.select_set(True)
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
for k in bpy.context.scene.keys():
del bpy.context.scene[k]
# save as gltf
bpy.ops.export_scene.gltf(
filepath=filepath,
check_existing=False,
export_format='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()
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', 'font3d']
# for addon in keep:
# bpy.ops.preferences.addon_enable(module=addon)
class FONT3D_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'}
def execute(self, context):
global shared
scene = bpy.context.scene
font3d = scene.font3d
font3d_data = scene.font3d_data
fontcollection = bpy.data.collections.get("Font3D")
if fontcollection is None:
fontcollection = bpy.data.collections.new("Font3D")
ifxsplit = font3d.import_infix.split('_')
font_name = f"{ifxsplit[1]}_{ifxsplit[2]}"
face_name = ifxsplit[3]
added_font = False
# TODO: do not clear
font3d_data.available_fonts.clear()
Font.fonts = {}
currentObjects = []
for o in context.selected_objects:
if o.name not in currentObjects:
if font3d.import_infix in o.name and not butils.is_metrics_obj(o):
uc = o.users_collection
regex = f"{font3d.import_infix}(.)*"
name = re.sub(regex, "", o.name)
glyph_id = "unknown"
if len(name) == 1:
glyph_id = name
elif name in Font.name_to_glyph_d:
glyph_id = Font.name_to_glyph_d[name]
if glyph_id != "unknown":
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)
added_font = True
#TODO: is there a better way to iterate over a CollectionProperty?
found = False
for f in font3d_data.available_fonts.values():
if f.font_name == font_name:
found = True
break
if not found:
f = font3d_data.available_fonts.add()
f.font_name = font_name
else:
self.report({'INFO'}, f"did not understand glyph {name}")
return {'FINISHED'}
class FONT3D_PT_RightPropertiesPanel(bpy.types.Panel):
"""Creates a Panel in the Object properties window"""
bl_label = f"{bl_info['name']}"
bl_idname = "FONT3D_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.font3d_data.available_texts if t.text_object == context.active_object), None)) != type(None)
is_glyph = type(next((t for t in context.scene.font3d_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
font3d = scene.font3d
font3d_data = scene.font3d_data
obj = context.active_object
def is_it_text():
return type(next((t for t in context.scene.font3d_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.font3d_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 = font3d_data.available_texts[font3d_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: {font3d_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, "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:")
classes = (
FONT3D_addonPreferences,
FONT3D_available_font,
FONT3D_glyph_properties,
FONT3D_text_properties,
FONT3D_data,
FONT3D_settings,
FONT3D_UL_fonts,
FONT3D_UL_texts,
FONT3D_PT_Panel,
FONT3D_PT_LoadFontPanel,
FONT3D_PT_FontList,
FONT3D_PT_TextPlacement,
FONT3D_PT_TextManagement,
FONT3D_PT_FontCreation,
FONT3D_PT_TextPropertiesPanel,
FONT3D_OT_AddDefaultMetrics,
FONT3D_OT_RemoveMetrics,
FONT3D_OT_AlignMetricsToActiveObject,
FONT3D_OT_AlignMetrics,
FONT3D_OT_TemporaryHelper,
FONT3D_OT_PlaceText,
FONT3D_OT_LoadFont,
FONT3D_OT_ToggleFont3DCollection,
FONT3D_OT_SaveFontToFile,
FONT3D_OT_CreateFontFromObjects,
FONT3D_PT_RightPropertiesPanel,
)
@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)
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.font3d_data.available_texts:
# TODO PERFORMANCE: only on demand
butils.set_text_on_curve(t)
# for i, t in enumerate(bpy.context.scene.font3d_data.available_texts):
# # TODO PERFORMANCE: only on demand
# # butils.set_text_on_curve(t)
# pass
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Scene.font3d = bpy.props.PointerProperty(type=FONT3D_settings)
bpy.types.Scene.font3d_data = bpy.props.PointerProperty(type=FONT3D_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_available_fonts)
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.font3d
del bpy.types.Scene.font3d_data
print(f"UNREGISTER {bl_info['name']}")
if __name__ == '__main__':
register()