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),
|
|
|
|
"location": "wherever it may be",
|
|
|
|
"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
|
2024-06-27 14:56:43 +02:00
|
|
|
import queue
|
2024-05-08 16:19:47 +02:00
|
|
|
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-08 16:19:47 +02:00
|
|
|
|
|
|
|
class SharedVariables():
|
2024-05-21 18:00:49 +02:00
|
|
|
fonts = Font.fonts
|
|
|
|
|
2024-05-08 16:19:47 +02:00
|
|
|
def __init__(self, **kv):
|
|
|
|
self.__dict__.update(kv)
|
|
|
|
|
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-08 16:19:47 +02:00
|
|
|
shared = SharedVariables()
|
|
|
|
|
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."))
|
|
|
|
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")
|
|
|
|
|
2024-05-08 16:19:47 +02:00
|
|
|
|
|
|
|
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):
|
2024-06-27 14:56:43 +02:00
|
|
|
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'},
|
|
|
|
)
|
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)
|
2024-07-10 16:34:43 +02:00
|
|
|
# TODO: update when animate
|
|
|
|
# does not work like this, somehow it does not run in main thread when the text is actually being set
|
|
|
|
# def get_float(self):
|
|
|
|
# return self["letter_spacingor"]
|
|
|
|
# def set_float(self, value):
|
|
|
|
# print(f"{utils.get_timestamp()} setting float to {value}")
|
|
|
|
# self["letter_spacingor"] = value
|
|
|
|
# def fun(text_properties : FONT3D_text_properties):
|
|
|
|
# # print(text_properties)
|
|
|
|
# # print(type(text_properties))
|
|
|
|
# # print(text_properties.letter_spacing)
|
|
|
|
# print(f"is running ---------------------------------->>>>>>> {text_properties.letter_spacing} and {text_properties.get('letter_spacingor')}")
|
|
|
|
# # butils.set_text_on_curve(text_properties)
|
|
|
|
# run_in_main_thread(lambda: fun(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)
|
2024-07-10 16:34:43 +02:00
|
|
|
text: bpy.props.StringProperty(
|
|
|
|
update=update_callback
|
|
|
|
)
|
2024-06-27 14:56:43 +02:00
|
|
|
letter_spacing: bpy.props.FloatProperty(
|
2024-07-10 16:34:43 +02:00
|
|
|
# get=get_float,
|
|
|
|
# set=set_float,
|
2024-06-27 14:56:43 +02:00
|
|
|
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)
|
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="")
|
|
|
|
active_text_index: bpy.props.IntProperty()
|
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))
|
|
|
|
# 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
|
|
|
|
|
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)
|
2024-07-10 16:34:43 +02:00
|
|
|
split.label(text="Id: %d" % (item.text_id))
|
2024-06-27 14:56:43 +02:00
|
|
|
# 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 #
|
2024-05-08 16:19:47 +02:00
|
|
|
|
|
|
|
class FONT3D_PT_panel(bpy.types.Panel):
|
|
|
|
bl_label = "Panel for Font3D"
|
|
|
|
bl_category = "Font3D"
|
|
|
|
bl_space_type = "VIEW_3D"
|
|
|
|
bl_region_type = "UI"
|
|
|
|
|
2024-06-27 14:56:43 +02:00
|
|
|
@classmethod
|
|
|
|
def poll(self, context):
|
|
|
|
scene = context.scene
|
|
|
|
font3d = scene.font3d
|
|
|
|
font3d_data = scene.font3d_data
|
|
|
|
# TODO: properly include this
|
2024-06-28 11:24:01 +02:00
|
|
|
def update():
|
2024-06-27 14:56:43 +02:00
|
|
|
font3d_data.active_text_index = -1
|
2024-06-28 11:24:01 +02:00
|
|
|
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:
|
2024-07-10 16:34:43 +02:00
|
|
|
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
|
2024-07-10 16:34:43 +02:00
|
|
|
# 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
|
|
|
|
|
2024-07-02 12:22:24 +02:00
|
|
|
if remove_me:
|
|
|
|
remove_list.append(i)
|
|
|
|
|
2024-06-28 11:24:01 +02:00
|
|
|
for i in remove_list:
|
|
|
|
font3d_data.available_texts.remove(i)
|
|
|
|
|
2024-07-10 16:34:43 +02:00
|
|
|
for i, t in enumerate(font3d_data.available_texts):
|
|
|
|
if context.active_object == t.text_object:
|
|
|
|
font3d_data.active_text_index = i
|
|
|
|
if (hasattr(context.active_object, "parent") and
|
|
|
|
context.active_object.parent == t.text_object):
|
|
|
|
font3d_data.active_text_index = i
|
2024-06-27 14:56:43 +02:00
|
|
|
|
2024-06-28 11:24:01 +02:00
|
|
|
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):
|
2024-05-21 18:00:49 +02:00
|
|
|
global shared
|
2024-05-08 16:19:47 +02:00
|
|
|
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-05-08 16:19:47 +02:00
|
|
|
layout.label(text="Load FontFile:")
|
|
|
|
layout.row().prop(font3d, "font_path")
|
|
|
|
layout.row().operator('font3d.loadfont', text='Load Font')
|
2024-05-21 18:00:49 +02:00
|
|
|
layout.label(text="Available Fonts")
|
|
|
|
layout.template_list("FONT3D_UL_fonts", "", font3d_data, "available_fonts", font3d_data, "active_font_index")
|
2024-05-08 16:19:47 +02:00
|
|
|
layout.label(text='DEBUG')
|
2024-05-28 14:11:32 +02:00
|
|
|
layout.row().prop(font3d, "test_text")
|
2024-06-27 14:56:43 +02:00
|
|
|
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")
|
2024-05-28 14:11:32 +02:00
|
|
|
layout.label(text="font creation")
|
2024-05-21 18:00:49 +02:00
|
|
|
layout.row().prop(font3d, "import_infix")
|
|
|
|
layout.row().operator('font3d.create_font_from_objects', text='Create Font')
|
2024-05-28 14:11:32 +02:00
|
|
|
layout.row().separator()
|
|
|
|
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-06-27 14:56:43 +02:00
|
|
|
layout.row().operator('font3d.temporaryhelper', text='Temporary Helper')
|
2024-05-21 18:00:49 +02:00
|
|
|
layout.label(text='DEBUG END')
|
|
|
|
|
2024-06-27 14:56:43 +02:00
|
|
|
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()
|
|
|
|
|
2024-06-28 11:24:29 +02:00
|
|
|
if type(props) == type(None) or type(props.text_object) == type(None):
|
|
|
|
# this should not happen
|
|
|
|
print("debug: this should not happen")
|
2024-06-27 14:56:43 +02:00
|
|
|
layout.label(text="AAAAH")
|
|
|
|
return
|
|
|
|
|
|
|
|
layout.label(text=f"Mom: {props.text_object.name}")
|
|
|
|
layout.row().prop(props, "letter_spacing")
|
2024-05-08 16:19:47 +02:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2024-05-21 18:00:49 +02:00
|
|
|
# print(f"loading da font at path {scene.font3d.font_path}")
|
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",
|
|
|
|
message=f"We believe the font path ({scene.font3d.font_path}) does not exist.",
|
|
|
|
)
|
|
|
|
return {'CANCELLED'}
|
2024-05-08 16:19:47 +02:00
|
|
|
|
|
|
|
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 = {}
|
2024-05-21 18:00:49 +02:00
|
|
|
shared.fonts[font['name']] = Font.Font()
|
|
|
|
shared.fonts[font['name']]['faces']['regular'] = font
|
2024-05-08 16:19:47 +02:00
|
|
|
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
2024-06-27 14:56:43 +02:00
|
|
|
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'}
|
2024-05-28 14:11:32 +02:00
|
|
|
|
2024-05-08 16:19:47 +02:00
|
|
|
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
|
|
|
|
|
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
|
|
|
|
|
|
|
|
if font3d.the_mother_of_typography:
|
|
|
|
selected = font3d.the_mother_of_typography
|
|
|
|
|
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'
|
|
|
|
|
2024-07-10 16:34:43 +02:00
|
|
|
text_id = 0
|
2024-07-02 12:22:24 +02:00
|
|
|
for i, tt in enumerate(font3d_data.available_texts):
|
2024-07-10 16:34:43 +02:00
|
|
|
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
|
|
|
|
t.text = scene.font3d.test_text
|
|
|
|
t.letter_spacing = 0.0
|
|
|
|
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.",
|
|
|
|
"You little piece of shit :)"),
|
|
|
|
icon='GHOST_ENABLED')
|
2024-05-21 18:00:49 +02:00
|
|
|
|
|
|
|
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'}
|
|
|
|
|
|
|
|
|
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'}
|
|
|
|
|
|
|
|
file_path = "whoopwhoop"
|
|
|
|
|
|
|
|
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):
|
|
|
|
"""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()
|
2024-05-28 14:11:32 +02:00
|
|
|
Font.fonts = {}
|
2024-05-21 18:00:49 +02:00
|
|
|
|
|
|
|
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":
|
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'}
|
|
|
|
|
2024-07-10 16:34:43 +02:00
|
|
|
class FONT3D_PT_RightPropertiesPanel(bpy.types.Panel):
|
2024-07-01 14:39:07 +02:00
|
|
|
"""Creates a Panel in the Object properties window"""
|
2024-07-10 16:34:43 +02:00
|
|
|
bl_label = f"{__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"
|
|
|
|
|
2024-07-10 16:34:43 +02:00
|
|
|
@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-07-10 16:34:43 +02:00
|
|
|
def is_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_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)
|
|
|
|
|
|
|
|
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
|
|
|
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()
|
2024-07-10 16:34:43 +02:00
|
|
|
row.label(text="text object is: " + textobject.name)
|
|
|
|
row = layout.row()
|
|
|
|
row.label(text=f"active text index is: {font3d_data.active_text_index}")
|
|
|
|
row = layout.row()
|
|
|
|
row.prop(available_text, "text")
|
2024-07-01 14:39:07 +02:00
|
|
|
row = layout.row()
|
2024-07-10 16:34:43 +02:00
|
|
|
row.prop(available_text, "letter_spacing")
|
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-08 16:19:47 +02:00
|
|
|
FONT3D_OT_Font3D,
|
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-05-08 16:19:47 +02:00
|
|
|
FONT3D_PT_panel,
|
2024-06-27 14:56:43 +02:00
|
|
|
FONT3D_PT_TextPropertiesPanel,
|
|
|
|
FONT3D_OT_TemporaryHelper,
|
2024-05-08 16:19:47 +02:00
|
|
|
FONT3D_OT_TestFont,
|
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,
|
2024-07-10 16:34:43 +02:00
|
|
|
FONT3D_PT_RightPropertiesPanel,
|
2024-05-08 16:19:47 +02:00
|
|
|
)
|
|
|
|
|
2024-07-10 16:34:43 +02:00
|
|
|
@persistent
|
|
|
|
def load_handler(self, dummy):
|
|
|
|
bpy.app.timers.register(execute_queued_functions)
|
|
|
|
|
|
|
|
def load_handler_unload():
|
|
|
|
bpy.app.timers.unregister(execute_queued_functions)
|
|
|
|
|
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)
|
2024-07-10 16:34:43 +02:00
|
|
|
# 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']}")
|
|
|
|
|
2024-07-10 16:34:43 +02:00
|
|
|
# auto start
|
|
|
|
if load_handler not in bpy.app.handlers.load_post:
|
|
|
|
bpy.app.handlers.load_post.append(load_handler)
|
2024-05-08 16:19:47 +02:00
|
|
|
|
2024-06-27 14:56:43 +02:00
|
|
|
# clear available fonts
|
|
|
|
def clear_available_fonts():
|
|
|
|
bpy.context.scene.font3d_data.available_fonts.clear()
|
|
|
|
|
2024-07-10 16:34:43 +02:00
|
|
|
def load_available_fonts():
|
|
|
|
global shared
|
|
|
|
preferences = getPreferences(bpy.context)
|
|
|
|
|
|
|
|
currentObjects = []
|
|
|
|
for ob in bpy.data.objects:
|
|
|
|
currentObjects.append(ob.name)
|
|
|
|
|
|
|
|
print(f"assets folder: {preferences.assets_dir}")
|
|
|
|
font_dir = f"{preferences.assets_dir}/fonts"
|
|
|
|
for file in os.listdir(font_dir):
|
|
|
|
if file.endswith(".glb"):
|
|
|
|
font_path = os.path.join(font_dir, file)
|
|
|
|
bpy.ops.import_scene.gltf(filepath=font_path)
|
|
|
|
|
|
|
|
fontcollection = bpy.data.collections.get("Font3D")
|
|
|
|
if fontcollection is None:
|
|
|
|
fontcollection = bpy.data.collections.new("Font3D")
|
|
|
|
|
|
|
|
remove_list = []
|
|
|
|
all_objects = []
|
|
|
|
for o in bpy.data.objects:
|
|
|
|
all_objects.append(o)
|
|
|
|
for o in all_objects:
|
|
|
|
if o.name not in currentObjects:
|
|
|
|
# must be new
|
|
|
|
if ("glyph" in o.keys()
|
|
|
|
and "face_name" in o.keys()
|
|
|
|
and "font_name" in o.keys()):
|
|
|
|
glyph_id = o["glyph"]
|
|
|
|
font_name = o["font_name"]
|
|
|
|
face_name = o["face_name"]
|
|
|
|
butils.move_in_fontcollection(
|
|
|
|
o,
|
|
|
|
fontcollection)
|
|
|
|
Font.add_glyph(
|
|
|
|
font_name,
|
|
|
|
face_name,
|
|
|
|
glyph_id,
|
|
|
|
o)
|
|
|
|
|
|
|
|
font3d_data = bpy.context.scene.font3d_data
|
|
|
|
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
|
|
|
|
print(f"font3d added {font_name}")
|
|
|
|
else:
|
|
|
|
remove_list.append(o)
|
|
|
|
for o in remove_list:
|
|
|
|
bpy.data.objects.remove(o, do_unlink=True)
|
|
|
|
|
2024-06-27 14:56:43 +02:00
|
|
|
run_in_main_thread(clear_available_fonts)
|
2024-07-10 16:34:43 +02:00
|
|
|
run_in_main_thread(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)
|
2024-07-10 16:34:43 +02:00
|
|
|
|
|
|
|
if load_handler in bpy.app.handlers.load_post:
|
|
|
|
bpy.app.handlers.load_post.remove(load_handler)
|
|
|
|
load_handler_unload()
|
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()
|
|
|
|
|