font3d_blender_addon/__init__.py

505 lines
16 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),
"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
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):
font_path: bpy.props.StringProperty(name="Font path",
2024-05-21 18:00:49 +02:00
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,
)
2024-05-28 14:11:32 +02:00
test_text: bpy.props.StringProperty(name="Test Text",
description="the text to test with",
default="Hello",
maxlen=1024,
)
2024-05-21 18:00:49 +02:00
class FONT3D_available_font(bpy.types.PropertyGroup):
font_name: bpy.props.StringProperty(name="whatever")
#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 poporotery")
active_font_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
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"
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-05-08 16:19:47 +02:00
layout.row().operator('font3d.testfont', text='Test Font')
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')
layout.label(text='DEBUG END')
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-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-05-28 14:11:32 +02:00
if selected:
font_name = "NM_Origin"
font_face = "Tender"
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
elif glyph.type == "CURVE":
print("is curve")
glyph_advance = (-1 * glyph.bound_box[0][0] + glyph.bound_box[4][0]) * 0.01
offset.x = advance
ob = bpy.data.objects.new(f"{glyph_id}", glyph.data)
ob.location = selected.location + offset
ob.scale = (0.01, 0.01, 0.01)
selected.users_collection[0].objects.link(ob)
ob.parent = selected
advance = advance + glyph_advance
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-05-28 16:53:01 +02:00
# needed to restore current state later
fontcollection_was_linked = False
previous_objects = []
fontcollection_objects = []
# hide fontcollection
if fontcollection is None:
self.report({'INFO'}, f"{bl_info['name']}: There is no collection")
return {'CANCELLED'}
elif scene.collection.children.find(fontcollection.name) > 0:
scene.collection.children.unlink(fontcollection)
fontcollection_was_linked = True
# collect and hide previous objects
for o in scene.objects:
previous_objects.append(o)
scene.collection.objects.unlink(o)
# show fontcollection
# if scene.collection.children.find(fontcollection.name) < 0:
# scene.collection.children.link(fontcollection)
# link fontcollection
for o in fontcollection.objects:
fontcollection_objects.append(o)
fontcollection.objects.unlink(o)
scene.collection.objects.link(o)
# get save data
selected_font = font3d_data.available_fonts[font3d_data.active_font_index]
2024-05-28 14:11:32 +02:00
if selected_font == "":
butils.ShowMessageBox("Warning", 'ERROR', "no font selected")
return {'CANCELLED'}
2024-05-28 16:53:01 +02:00
print(selected_font.font_name)
# save as gltf
bpy.ops.export_scene.gltf(
filepath="/home/jrkb/Downloads/toast/maker.gltf",
check_existing=False,
export_format='GLTF_SEPARATE',
export_extras=True,
export_hierarchy_full_collections=True,
use_active_collection_with_nested=True,
)
# restore from previous state
def restore():
for o in scene.objects:
scene.collection.objects.unlink(o)
for o in previous_objects:
scene.collection.objects.link(o)
for o in fontcollection_objects:
fontcollection.objects.link(o)
if fontcollection_was_linked:
scene.collection.children.link(fontcollection)
bpy.app.timers.register(restore, first_interval=5)
2024-05-28 14:11:32 +02:00
return {'FINISHED'}
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":
bpy.data.objects[o.name]["glyph"] = glyph_id
butils.move_in_fontcollection(
o,
fontcollection,
scene)
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-05-28 14:11:32 +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,
FONT3D_data,
2024-05-08 16:19:47 +02:00
FONT3D_settings,
2024-05-21 18:00:49 +02:00
FONT3D_UL_fonts,
2024-05-08 16:19:47 +02:00
FONT3D_PT_panel,
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-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)
print(f"REGISTER {bl_info['name']}")
2024-05-08 16:19:47 +02:00
# 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)
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)
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()