font3d_blender_addon/__init__.py

934 lines
33 KiB
Python
Raw Normal View History

2024-05-08 16:19:47 +02:00
# 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),
2024-08-04 12:52:37 +02:00
"location": "VIEW3D",
2024-05-08 16:19:47 +02:00
"description": "Does Font3D stuff",
"category": "Typography",
}
2024-05-21 18:00:49 +02:00
# 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
2024-05-08 16:19:47 +02:00
import bpy
import math
2024-05-28 14:11:32 +02:00
import mathutils
2024-05-08 16:19:47 +02:00
import io
import functools
from bpy.types import Panel
from bpy.app.handlers import persistent
from random import uniform
import time
import datetime
2024-05-28 14:11:32 +02:00
import os
2024-05-21 18:00:49 +02:00
import re
2024-05-28 14:11:32 +02:00
def getPreferences(context):
preferences = context.preferences
return preferences.addons[__name__].preferences
2024-05-21 18:00:49 +02:00
2024-05-28 14:11:32 +02:00
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")
2024-05-08 16:19:47 +02:00
# class FONT3D_OT_Font3D(bpy.types.Operator):
# """Font 3D"""
# bl_idname = f"{__name__}.font3d"
# bl_label = "Font 3D"
# bl_options = {'REGISTER', 'UNDO'}
2024-05-08 16:19:47 +02:00
# def execute(self, context):
2024-05-08 16:19:47 +02:00
# print("Font3d execute()")
2024-05-08 16:19:47 +02:00
# scene = bpy.context.scene
2024-05-08 16:19:47 +02:00
# file_dir = scene.font3d.file_dir
# print(f"file_dir: {file_dir}")
2024-05-08 16:19:47 +02:00
# return {'FINISHED'}
2024-05-08 16:19:47 +02:00
class FONT3D_settings(bpy.types.PropertyGroup):
2024-06-27 14:56:43 +02:00
font_path: bpy.props.StringProperty(
name="Font path",
2024-08-04 12:52:37 +02:00
description="Load a *.glb or *.gltf fontfile from disk",
2024-06-27 14:56:43 +02:00
default="",
maxlen=1024,
subtype="FILE_PATH")
import_infix: bpy.props.StringProperty(
name="Font name import infix",
2024-08-04 13:31:06 +02:00
description="The infix which all font objects to import have. obj name: 'A_NM_Origin_Tender' -> infix: '_NM_Origin_Tender'",
2024-06-27 14:56:43 +02:00
default="_NM_",
maxlen=1024,
)
2024-08-04 12:52:37 +02:00
text: bpy.props.StringProperty(
name="Text",
description="The text.",
2024-06-27 14:56:43 +02:00
default="HELLO",
maxlen=1024,
)
2024-08-04 12:52:37 +02:00
target_object: bpy.props.PointerProperty(
name="The Target Object",
2024-06-27 14:56:43 +02:00
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,
2024-08-04 12:52:37 +02:00
)
orientation: bpy.props.FloatVectorProperty(
name="Orientation",
default=(1.5707963267948966, 0.0, 0.0), # 90 degrees in radians
subtype='EULER',
2024-06-27 14:56:43 +02:00
)
2024-05-21 18:00:49 +02:00
class FONT3D_available_font(bpy.types.PropertyGroup):
2024-06-27 14:56:43 +02:00
font_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()
2024-06-27 14:56:43 +02:00
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
)
2024-06-27 14:56:43 +02:00
letter_spacing: bpy.props.FloatProperty(
update=update_callback,
name="Letter Spacing",
description="Letter Spacing",
2024-08-04 12:52:37 +02:00
options={'ANIMATABLE'},
2024-06-27 14:56:43 +02:00
step=0.01,
)
2024-08-04 12:52:37 +02:00
orientation: bpy.props.FloatVectorProperty(
update=update_callback,
name="Orientation",
default=(1.5707963267948966, 0.0, 0.0), # 90 degrees in radians
subtype='EULER',
)
2024-08-05 12:59:07 +02:00
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',
)
2024-06-27 14:56:43 +02:00
distribution_type: bpy.props.StringProperty()
glyphs: bpy.props.CollectionProperty(type=FONT3D_glyph_properties)
2024-05-21 18:00:49 +02:00
#TODO: simply, merge, cut cut cut
class FONT3D_data(bpy.types.PropertyGroup):
2024-06-27 14:56:43 +02:00
available_fonts: bpy.props.CollectionProperty(type=FONT3D_available_font, name="name of the collection property")
2024-05-21 18:00:49 +02:00
active_font_index: bpy.props.IntProperty()
2024-06-27 14:56:43 +02:00
available_texts: bpy.props.CollectionProperty(type=FONT3D_text_properties, name="")
2024-08-04 12:52:37 +02:00
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)
2024-05-21 18:00:49 +02:00
class FONT3D_UL_fonts(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="Index: %d" % (index))
split.label(text=f"{item.font_name}") # avoids renaming the item by accident
def invoke(self, context, event):
pass
2024-06-27 14:56:43 +02:00
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))
2024-06-27 14:56:43 +02:00
split.label(text=f"{item.text}") # avoids renaming the item by accident
def invoke(self, context, event):
pass
2024-08-04 12:52:37 +02:00
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"
2024-08-04 13:32:01 +02:00
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
2024-08-04 12:52:37 +02:00
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.column().prop(font3d, "orientation")
2024-08-04 13:32:01 +02:00
placerow = layout.row()
placerow.enabled = self.can_place
2024-08-05 12:59:45 +02:00
placerow.operator(f"{__name__}.placetext", text='Place Text')
2024-08-04 13:32:01 +02:00
if not self.can_place:
layout.label(text="Cannot place Text.")
layout.label(text="Select a curve as active object.")
2024-05-08 16:19:47 +02:00
2024-08-04 12:52:37 +02:00
class FONT3D_PT_TextManagement(bpy.types.Panel):
bl_label = "Text Management"
bl_parent_id = "FONT3D_PT_Panel"
2024-05-08 16:19:47 +02:00
bl_category = "Font3D"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
2024-08-04 12:52:37 +02:00
# TODO: perhaps this should be done in a periodic timer
2024-06-27 14:56:43 +02:00
@classmethod
def poll(self, context):
scene = context.scene
font3d = scene.font3d
font3d_data = scene.font3d_data
2024-08-04 12:52:37 +02:00
# TODO: update available_texts
def update():
2024-08-04 12:52:37 +02:00
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)
2024-07-02 12:22:24 +02:00
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:
2024-07-02 12:22:24 +02:00
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)
2024-08-04 12:52:37 +02:00
# 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
2024-07-02 12:22:24 +02:00
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:
2024-08-04 12:52:37 +02:00
active_text_index = i
if (hasattr(context.active_object, "parent") and
context.active_object.parent == t.text_object):
2024-08-04 12:52:37 +02:00
active_text_index = i
if active_text_index != font3d_data.active_text_index:
font3d_data.active_text_index = active_text_index
2024-06-27 14:56:43 +02:00
butils.run_in_main_thread(update)
2024-06-27 14:56:43 +02:00
return True
2024-05-08 16:19:47 +02:00
def draw(self, context):
layout = self.layout
wm = context.window_manager
scene = context.scene
font3d = scene.font3d
2024-05-21 18:00:49 +02:00
font3d_data = scene.font3d_data
2024-06-27 14:56:43 +02:00
layout.label(text="Text Objects")
layout.template_list("FONT3D_UL_texts", "", font3d_data, "available_texts", font3d_data, "active_text_index")
2024-08-04 12:52:37 +02:00
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
2024-08-07 11:56:13 +02:00
layout.row().label(text="Font name import infix:")
layout.row().prop(font3d, "import_infix", text="")
2024-05-21 18:00:49 +02:00
layout.row().operator('font3d.create_font_from_objects', text='Create Font')
2024-05-28 14:11:32 +02:00
layout.row().operator('font3d.save_font_to_file', text='Save Font To File')
2024-05-21 18:00:49 +02:00
layout.row().operator('font3d.toggle_font3d_collection', text='Toggle Collection')
2024-08-07 11:56:13 +02:00
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')
2024-05-21 18:00:49 +02:00
2024-06-27 14:56:43 +02:00
class FONT3D_PT_TextPropertiesPanel(bpy.types.Panel):
bl_label = "Text Properties"
2024-08-04 12:52:37 +02:00
bl_parent_id = "FONT3D_PT_TextManagement"
2024-06-27 14:56:43 +02:00
bl_category = "Font3D"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
def get_active_text_properties(self):
2024-08-04 15:12:38 +02:00
if type(bpy.context.active_object) != type(None):# and bpy.context.object.select_get():
2024-06-27 14:56:43 +02:00
for t in bpy.context.scene.font3d_data.available_texts:
2024-08-04 15:12:38 +02:00
if bpy.context.active_object == t.text_object:
2024-06-27 14:56:43 +02:00
return t
2024-08-04 15:12:38 +02:00
if bpy.context.active_object.parent == t.text_object:
2024-06-27 14:56:43 +02:00
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()
2024-06-28 11:24:29 +02:00
if type(props) == type(None) or type(props.text_object) == type(None):
# this should not happen
2024-08-04 12:52:37 +02:00
# as then polling does not work
# however, we are paranoid
2024-06-27 14:56:43 +02:00
return
layout.label(text=f"Mom: {props.text_object.name}")
2024-08-04 12:52:37 +02:00
layout.row().prop(props, "text")
2024-06-27 14:56:43 +02:00
layout.row().prop(props, "letter_spacing")
2024-08-05 12:59:07 +02:00
layout.row().prop(props, "font_size")
layout.column().prop(props, "translation")
2024-08-04 12:52:37 +02:00
layout.column().prop(props, "orientation")
2024-05-08 16:19:47 +02:00
class FONT3D_OT_LoadFont(bpy.types.Operator):
2024-08-04 12:52:37 +02:00
"""Load Fontfile from path above.
(Format must be *.glb or *.gltf)"""
bl_idname = f"{__name__}.loadfont"
2024-05-08 16:19:47 +02:00
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'}
2024-05-28 14:11:32 +02:00
if not os.path.exists(scene.font3d.font_path):
butils.ShowMessageBox(
title=f"{__name__} Warning",
icon="ERROR",
2024-08-04 12:52:37 +02:00
message=f"We believe the font path ({scene[__name__].font_path}) does not exist.",
2024-05-28 14:11:32 +02:00
)
return {'CANCELLED'}
2024-05-08 16:19:47 +02:00
2024-08-04 12:52:37 +02:00
butils.load_font_from_filepath(scene.font3d.font_path)
2024-05-08 16:19:47 +02:00
return {'FINISHED'}
2024-08-07 11:56:13 +02:00
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'}
2024-06-27 14:56:43 +02:00
class FONT3D_OT_TemporaryHelper(bpy.types.Operator):
"""Temp Font 3D"""
2024-08-04 12:52:37 +02:00
bl_idname = f"{__name__}.temporaryhelper"
2024-06-27 14:56:43 +02:00
bl_label = "Temp Font"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
global shared
scene = bpy.context.scene
font3d_data = scene.font3d_data
2024-08-07 11:56:13 +02:00
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)
2024-06-27 14:56:43 +02:00
return {'FINISHED'}
2024-05-28 14:11:32 +02:00
2024-08-04 12:52:37 +02:00
class FONT3D_OT_PlaceText(bpy.types.Operator):
2024-08-04 13:32:01 +02:00
"""Place Text 3D on active object"""
2024-08-04 12:52:37 +02:00
bl_idname = f"{__name__}.placetext"
bl_label = "Place Text"
2024-05-08 16:19:47 +02:00
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
global shared
scene = bpy.context.scene
2024-05-21 18:00:49 +02:00
selected = bpy.context.view_layer.objects.active
2024-06-27 14:56:43 +02:00
font3d = scene.font3d
font3d_data = scene.font3d_data
2024-08-04 12:52:37 +02:00
if font3d.target_object:
selected = font3d.target_object
2024-06-27 14:56:43 +02:00
2024-05-28 14:11:32 +02:00
if selected:
font_name = "NM_Origin"
font_face = "Tender"
2024-06-27 14:56:43 +02:00
distribution_type = 'DEFAULT'
text_id = 0
2024-07-02 12:22:24 +02:00
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
2024-06-27 14:56:43 +02:00
t.font_name = font_name
t.font_face = font_face
t.text_object = selected
2024-08-04 12:52:37 +02:00
t.text = scene.font3d.text
t.letter_spacing = scene.font3d.letter_spacing
t.orientation = scene.font3d.orientation
2024-06-27 14:56:43 +02:00
t.distribution_type = distribution_type
2024-05-28 14:11:32 +02:00
else:
butils.ShowMessageBox(
title="No object selected",
message=(
"Please select an object.",
"It will be used to put the type on.",
2024-08-04 12:52:37 +02:00
"Thank you :)"),
2024-05-28 14:11:32 +02:00
icon='GHOST_ENABLED')
2024-05-21 18:00:49 +02:00
return {'FINISHED'}
class FONT3D_OT_ToggleFont3DCollection(bpy.types.Operator):
"""Toggle Font3D Collection"""
2024-08-04 12:52:37 +02:00
bl_idname = f"{__name__}.toggle_font3d_collection"
2024-05-21 18:00:49 +02:00
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'}
2024-05-28 14:11:32 +02:00
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
2024-05-28 16:53:01 +02:00
fontcollection = bpy.data.collections.get("Font3D")
2024-05-28 14:11:32 +02:00
2024-06-27 14:56:43 +02:00
# check if all is good to proceed
2024-05-28 16:53:01 +02:00
if fontcollection is None:
self.report({'INFO'}, f"{bl_info['name']}: There is no collection")
return {'CANCELLED'}
2024-06-27 14:56:43 +02:00
if font3d_data.active_font_index < 0:
self.report({'INFO'}, f"{bl_info['name']}: There is no active font")
return {'CANCELLED'}
2024-05-28 16:53:01 +02:00
2024-06-27 14:56:43 +02:00
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")
2024-05-28 16:53:01 +02:00
# get save data
selected_font = font3d_data.available_fonts[font3d_data.active_font_index]
2024-05-28 14:11:32 +02:00
2024-07-01 14:39:07 +02:00
# print(selected_font.font_name)
2024-06-27 14:56:43 +02:00
self.report({'INFO'}, f"{bl_info['name']}: {selected_font.font_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:
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):
2024-06-28 10:24:22 +02:00
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]
2024-06-27 14:56:43 +02:00
# save as gltf
2024-06-28 10:24:56 +02:00
bpy.ops.export_scene.gltf(
2024-06-27 14:56:43 +02:00
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)
2024-05-28 16:53:01 +02:00
2024-06-27 14:56:43 +02:00
bpy.ops.scene.delete()
# restore()
2024-05-28 16:53:01 +02:00
2024-05-28 14:11:32 +02:00
return {'FINISHED'}
2024-06-27 14:56:43 +02:00
# 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)
2024-05-28 14:11:32 +02:00
2024-05-21 18:00:49 +02:00
class FONT3D_OT_CreateFontFromObjects(bpy.types.Operator):
2024-08-07 11:56:13 +02:00
"""Create Font from selected objects"""
2024-08-04 12:52:37 +02:00
bl_idname = f"{__name__}.create_font_from_objects"
2024-05-21 18:00:49 +02:00
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
2024-08-07 11:56:13 +02:00
# TODO: do not clear
2024-05-21 18:00:49 +02:00
font3d_data.available_fonts.clear()
2024-05-28 14:11:32 +02:00
Font.fonts = {}
2024-05-21 18:00:49 +02:00
currentObjects = []
2024-08-07 11:56:13 +02:00
for o in context.selected_objects:
2024-05-21 18:00:49 +02:00
if o.name not in currentObjects:
2024-08-07 11:56:13 +02:00
if font3d.import_infix in o.name and not butils.is_metrics_obj(o):
2024-05-21 18:00:49 +02:00
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":
2024-06-27 14:56:43 +02:00
o["glyph"] = glyph_id
o["font_name"] = font_name
o["face_name"] = face_name
# butils.apply_all_transforms(o)
2024-05-21 18:00:49 +02:00
butils.move_in_fontcollection(
o,
2024-06-27 14:56:43 +02:00
fontcollection)
2024-05-21 18:00:49 +02:00
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}")
2024-05-08 16:19:47 +02:00
return {'FINISHED'}
class FONT3D_PT_RightPropertiesPanel(bpy.types.Panel):
2024-07-01 14:39:07 +02:00
"""Creates a Panel in the Object properties window"""
2024-08-04 12:52:37 +02:00
bl_label = f"{bl_info['name']}"
bl_idname = "FONT3D_PT_RightPropertiesPanel"
2024-07-01 14:39:07 +02:00
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
2024-07-01 14:39:07 +02:00
def draw(self, context):
layout = self.layout
scene = context.scene
font3d = scene.font3d
font3d_data = scene.font3d_data
obj = context.active_object
2024-08-05 12:59:07 +02:00
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)
2024-08-05 12:59:07 +02:00
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)
2024-08-05 12:59:07 +02:00
is_text = is_it_text()
is_glyph = is_it_glyph()
2024-08-05 12:59:07 +02:00
textobject = obj if is_text else obj.parent if is_glyph else obj
available_text = font3d_data.available_texts[font3d_data.active_text_index]
2024-07-01 14:39:07 +02:00
2024-08-05 12:59:07 +02:00
# 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}")
2024-08-05 12:59:07 +02:00
layout.row().label(text="Text Properties:")
2024-08-04 12:52:37 +02:00
layout.row().prop(available_text, "text")
layout.row().prop(available_text, "letter_spacing")
2024-08-05 12:59:07 +02:00
layout.row().prop(available_text, "font_size")
layout.column().prop(available_text, "translation")
2024-08-04 12:52:37 +02:00
layout.column().prop(available_text, "orientation")
2024-07-01 14:39:07 +02:00
2024-08-05 12:59:07 +02:00
if is_glyph:
layout.row().label(text="Glyph Properties:")
2024-07-01 14:39:07 +02:00
2024-05-08 16:19:47 +02:00
classes = (
2024-05-28 14:11:32 +02:00
FONT3D_addonPreferences,
2024-05-21 18:00:49 +02:00
FONT3D_available_font,
2024-06-27 14:56:43 +02:00
FONT3D_glyph_properties,
FONT3D_text_properties,
2024-05-21 18:00:49 +02:00
FONT3D_data,
2024-05-08 16:19:47 +02:00
FONT3D_settings,
2024-05-21 18:00:49 +02:00
FONT3D_UL_fonts,
2024-06-27 14:56:43 +02:00
FONT3D_UL_texts,
2024-08-04 12:52:37 +02:00
FONT3D_PT_Panel,
FONT3D_PT_LoadFontPanel,
FONT3D_PT_FontList,
FONT3D_PT_TextPlacement,
FONT3D_PT_TextManagement,
FONT3D_PT_FontCreation,
2024-06-27 14:56:43 +02:00
FONT3D_PT_TextPropertiesPanel,
2024-08-07 11:56:13 +02:00
FONT3D_OT_AddDefaultMetrics,
FONT3D_OT_RemoveMetrics,
FONT3D_OT_AlignMetricsToActiveObject,
FONT3D_OT_AlignMetrics,
2024-06-27 14:56:43 +02:00
FONT3D_OT_TemporaryHelper,
2024-08-04 12:52:37 +02:00
FONT3D_OT_PlaceText,
2024-05-21 18:00:49 +02:00
FONT3D_OT_LoadFont,
FONT3D_OT_ToggleFont3DCollection,
2024-05-28 14:11:32 +02:00
FONT3D_OT_SaveFontToFile,
2024-05-21 18:00:49 +02:00
FONT3D_OT_CreateFontFromObjects,
FONT3D_PT_RightPropertiesPanel,
2024-05-08 16:19:47 +02:00
)
@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)
2024-08-04 12:52:37 +02:00
@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
2024-05-08 16:19:47 +02:00
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Scene.font3d = bpy.props.PointerProperty(type=FONT3D_settings)
2024-05-21 18:00:49 +02:00
bpy.types.Scene.font3d_data = bpy.props.PointerProperty(type=FONT3D_data)
# bpy.types.Object.__del__ = lambda self: print(f"Bye {self.name}")
2024-05-21 18:00:49 +02:00
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)
2024-05-08 16:19:47 +02:00
2024-08-04 12:52:37 +02:00
if on_frame_changed not in bpy.app.handlers.frame_change_post:
bpy.app.handlers.frame_change_post.append(on_frame_changed)
2024-06-27 14:56:43 +02:00
2024-08-04 12:52:37 +02:00
butils.run_in_main_thread(butils.clear_available_fonts)
butils.run_in_main_thread(butils.load_available_fonts)
2024-06-27 14:56:43 +02:00
2024-05-08 16:19:47 +02:00
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()
2024-05-08 16:19:47 +02:00
2024-08-04 12:52:37 +02:00
if on_frame_changed in bpy.app.handlers.frame_change_post:
bpy.app.handlers.frame_change_post.remove(on_frame_changed)
2024-05-08 16:19:47 +02:00
del bpy.types.Scene.font3d
2024-05-21 18:00:49 +02:00
del bpy.types.Scene.font3d_data
print(f"UNREGISTER {bl_info['name']}")
2024-05-08 16:19:47 +02:00
if __name__ == '__main__':
register()