font3d_blender_addon/__init__.py
themancalledjakob c1a3afbd60 clean
2024-06-28 10:24:56 +02:00

731 lines
25 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": "wherever it may be",
"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 queue
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
class SharedVariables():
fonts = Font.fonts
def __init__(self, **kv):
self.__dict__.update(kv)
def getPreferences(context):
preferences = context.preferences
return preferences.addons[__name__].preferences
shared = SharedVariables()
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."))
else:
shared.paths["assets"] = self.assets_dir
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_OT_Font3D(bpy.types.Operator):
"""Font 3D"""
bl_idname = "font3d.font3d"
bl_label = "Font 3D"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
global shared
print("Font3d execute()")
scene = bpy.context.scene
file_dir = scene.font3d.file_dir
print(f"file_dir: {file_dir}")
return {'FINISHED'}
class FONT3D_settings(bpy.types.PropertyGroup):
font_path: bpy.props.StringProperty(
name="Font path",
description="Where is the font",
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",
default="_NM_",
maxlen=1024,
)
test_text: bpy.props.StringProperty(
name="Test Text",
description="the text to test with",
default="HELLO",
maxlen=1024,
)
the_mother_of_typography: bpy.props.PointerProperty(
name="The Mother Of Typography",
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,
options={'ANIMATABLE'},
)
class FONT3D_available_font(bpy.types.PropertyGroup):
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)
font_name: bpy.props.StringProperty()
font_face: bpy.props.StringProperty()
text_object: bpy.props.PointerProperty(type=bpy.types.Object)
text: bpy.props.StringProperty()
letter_spacing: bpy.props.FloatProperty(
update=update_callback,
name="Letter Spacing",
description="Letter Spacing",
step=0.01,
)
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="")
active_text_index: bpy.props.IntProperty()
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))
# custom_icon = "OUTLINER_OB_%s" % item.obj_type
# split.prop(item, "name", text="", emboss=False, translate=False)
split.label(text=f"{item.font_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="Index: %d" % (index))
# custom_icon = "OUTLINER_OB_%s" % item.obj_type
# split.prop(item, "name", text="", emboss=False, translate=False)
split.label(text=f"{item.text}") # avoids renaming the item by accident
def invoke(self, context, event):
pass
# TODO: TODO: TODO: TODO: TODO # >>>>>>>>>>>>>>>>
execution_queue = queue.Queue()
# This function can safely be called in another thread.
# The function will be executed when the timer runs the next time.
def run_in_main_thread(function):
execution_queue.put(function)
def execute_queued_functions():
while not execution_queue.empty():
function = execution_queue.get()
function()
return 1.0
# <<<<<<<<<<<<<<<<< TODO: TODO: TODO: TODO: TODO #
class FONT3D_PT_panel(bpy.types.Panel):
bl_label = "Panel for Font3D"
bl_category = "Font3D"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
@classmethod
def poll(self, context):
scene = context.scene
font3d = scene.font3d
font3d_data = scene.font3d_data
# TODO: properly include this
def lol():
font3d_data.active_text_index = -1
# print(f"{utils.get_timestamp()} ors something")
run_in_main_thread(lol)
return True
def draw(self, context):
global shared
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')
layout.label(text="Available Fonts")
layout.template_list("FONT3D_UL_fonts", "", font3d_data, "available_fonts", font3d_data, "active_font_index")
layout.label(text='DEBUG')
layout.row().prop(font3d, "test_text")
layout.row().prop(font3d, "the_mother_of_typography")
layout.row().operator('font3d.testfont', text='Distribute')
layout.label(text="Text Objects")
layout.template_list("FONT3D_UL_texts", "", font3d_data, "available_texts", font3d_data, "active_text_index")
layout.label(text="font properties")
layout.row().prop(font3d, "letter_spacing")
layout.label(text="font creation")
layout.row().prop(font3d, "import_infix")
layout.row().operator('font3d.create_font_from_objects', text='Create Font')
layout.row().separator()
layout.row().operator('font3d.save_font_to_file', text='Save Font To File')
layout.row().operator('font3d.toggle_font3d_collection', text='Toggle Collection')
layout.row().operator('font3d.temporaryhelper', text='Temporary Helper')
layout.label(text='DEBUG END')
class FONT3D_PT_TextPropertiesPanel(bpy.types.Panel):
bl_label = "Text Properties"
bl_parent_id = "FONT3D_PT_panel"
bl_category = "Font3D"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
def get_active_text_properties(self):
if type(bpy.context.object) != type(None) and bpy.context.object.select_get():
for t in bpy.context.scene.font3d_data.available_texts:
if bpy.context.object == t.text_object:
return t
if bpy.context.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):
global shared
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):
layout.label(text="AAAAH")
return
layout.label(text=f"Mom: {props.text_object.name}")
layout.row().prop(props, "letter_spacing")
class FONT3D_OT_LoadFont(bpy.types.Operator):
"""Load Font 3D"""
bl_idname = "font3d.loadfont"
bl_label = "Load Font"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
global shared
scene = bpy.context.scene
# print(f"loading da font at path {scene.font3d.font_path}")
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.font3d.font_path}) does not exist.",
)
return {'CANCELLED'}
currentObjects = []
for ob in bpy.data.objects:
currentObjects.append(ob.name)
bpy.ops.import_scene.gltf(filepath=scene.font3d.font_path)
newObjects = []
fontcollection = bpy.data.collections.new("Font3D")
scene.collection.children.link(fontcollection)
font = {
"name": "",
"glyphs": []
}
for o in bpy.data.objects:
if o.name not in currentObjects:
if (o.parent == None):
font['name'] = o.name
elif o.parent.name.startswith("glyphs"):
font['glyphs'].append(o)
newObjects.append(o.name)
scene.collection.objects.unlink(o)
fontcollection.objects.link(o)
try:
shared.fonts
except:
shared.fonts = {}
shared.fonts[font['name']] = Font.Font()
shared.fonts[font['name']]['faces']['regular'] = font
return {'FINISHED'}
class FONT3D_OT_TemporaryHelper(bpy.types.Operator):
"""Temp Font 3D"""
bl_idname = "font3d.temporaryhelper"
bl_label = "Temp Font"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
global shared
scene = bpy.context.scene
font3d_data = scene.font3d_data
font3d_data.available_texts.clear()
return {'FINISHED'}
class FONT3D_OT_TestFont(bpy.types.Operator):
"""Test Font 3D"""
bl_idname = "font3d.testfont"
bl_label = "Test Font"
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.the_mother_of_typography:
selected = font3d.the_mother_of_typography
if selected:
font_name = "NM_Origin"
font_face = "Tender"
# text_text_object = bpy.data.objects.new(f"{selected.name}_text", None)
# text_text_object.empty_display_type = 'PLAIN_AXES'
# selected.users_collection[0].objects.link(text_text_object)
distribution_type = 'DEFAULT'
bpy.ops.object.select_all(action='DESELECT')
t = font3d_data.available_texts.add()
offset = mathutils.Vector((0.0, 0.0, 0.0))
advance = 0
for i, c in enumerate(scene.font3d.test_text):
glyph_id = c
glyph = Font.get_glyph(font_name, font_face, glyph_id)
if glyph == None:
self.report({'ERROR'}, f"Glyph not found for {font_name} {font_face} {glyph_id}")
continue
ob = bpy.data.objects.new(f"{glyph_id}", glyph.data)
if selected.type == "CURVE":
distribution_type = "FOLLOW_PATH"
curve_length = butils.get_curve_length(selected)
ob.constraints.new(type='FOLLOW_PATH')
ob.constraints["Follow Path"].target = selected
ob.constraints["Follow Path"].use_fixed_location = True
ob.constraints["Follow Path"].offset_factor = advance / curve_length
ob.constraints["Follow Path"].use_curve_follow = True
ob.constraints["Follow Path"].forward_axis = "FORWARD_X"
ob.constraints["Follow Path"].up_axis = "UP_Y"
# butils.ShowMessageBox("WHAT","INFO","I don't really know what you mean, lsaidry")
else:
offset.x = advance
ob.location = selected.location + offset
scalor = 0.001
glyph_advance = (-1 * glyph.bound_box[0][0] + glyph.bound_box[4][0]) * scalor
ob.scale = (scalor, scalor, scalor)
selected.users_collection[0].objects.link(ob)
advance = advance + glyph_advance
tc = t.glyphs.add()
tc.glyph_id = c
tc.glyph_object = ob
tc.letter_spacing = 0
ob.select_set(True)
selected.select_set(True)
bpy.context.view_layer.objects.active = selected
bpy.ops.object.parent_set(type='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
t.font_name = font_name
t.font_face = font_face
t.text_object = selected
t.text = scene.font3d.test_text
t.letter_spacing = 0.0
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.",
"You little piece of shit :)"),
icon='GHOST_ENABLED')
return {'FINISHED'}
class FONT3D_OT_ToggleFont3DCollection(bpy.types.Operator):
"""Toggle Font3D Collection"""
bl_idname = "font3d.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'}
file_path = "whoopwhoop"
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
# create restore function
# def restore():
# bpy.ops.object.select_all(action="DESELECT")
# for obj in was_selection:
# obj.select_set(True)
# bpy.context.view_layer.objects.active = was_active_object
# if not was_fontcollection_linked:
# scene.collection.children.unlink(fontcollection)
# # show fontcollection
# if not was_fontcollection_linked:
# scene.collection.children.link(fontcollection)
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}")
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):
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 open objects"""
bl_idname = "font3d.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
font3d_data.available_fonts.clear()
Font.fonts = {}
currentObjects = []
for o in bpy.data.objects:
if o.name not in currentObjects:
if font3d.import_infix in o.name:
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'}
classes = (
FONT3D_addonPreferences,
FONT3D_OT_Font3D,
FONT3D_available_font,
FONT3D_glyph_properties,
FONT3D_text_properties,
FONT3D_data,
FONT3D_settings,
FONT3D_UL_fonts,
FONT3D_UL_texts,
FONT3D_PT_panel,
FONT3D_PT_TextPropertiesPanel,
FONT3D_OT_TemporaryHelper,
FONT3D_OT_TestFont,
FONT3D_OT_LoadFont,
FONT3D_OT_ToggleFont3DCollection,
FONT3D_OT_SaveFontToFile,
FONT3D_OT_CreateFontFromObjects,
)
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']}")
bpy.app.timers.register(execute_queued_functions)
# would love to properly auto start this, but IT DOES NOT WORK
# if load_handler not in bpy.app.handlers.load_post:
# bpy.app.handlers.load_post.append(load_handler)
# clear available fonts
def clear_available_fonts():
bpy.context.scene.font3d_data.available_fonts.clear()
run_in_main_thread(clear_available_fonts)
def unregister():
# would love to properly auto start this, but IT DOES NOT WORK
# if load_handler in bpy.app.handlers.load_post:
# bpy.app.handlers.load_post.remove(load_handler)
for cls in classes:
bpy.utils.unregister_class(cls)
bpy.app.timers.unregister(execute_queued_functions)
del bpy.types.Scene.font3d
del bpy.types.Scene.font3d_data
print(f"UNREGISTER {bl_info['name']}")
if __name__ == '__main__':
register()