f6b1649c71
when we set a text, we don't want our manual depsgraph update, because this would trigger another setting of the text, which would trigger the manual depsgraph update, which would trigger another setting of the text, which would trigger the manual depsgraph update, which will eventually be too much. So, we ignore the depsgraph update n-times, where "n = n-letters + 1". worst side effect: might not update the text automatically in edge cases.
1687 lines
61 KiB
Python
1687 lines
61 KiB
Python
# SPDX-License-Identifier: GPL-2.0-only
|
|
|
|
"""
|
|
A 3D font helper
|
|
"""
|
|
|
|
import os
|
|
from bpy.app.handlers import persistent
|
|
from bpy.types import Panel
|
|
import functools
|
|
import io
|
|
import bpy
|
|
import importlib
|
|
|
|
bl_info = {
|
|
"name": "ABC3D",
|
|
"author": "Jakob Schlötter, Studio Pointer*",
|
|
"version": (0, 0, 2),
|
|
"blender": (4, 1, 0),
|
|
"location": "VIEW3D",
|
|
"description": "Convenience addon for 3D fonts",
|
|
"category": "Typography",
|
|
}
|
|
|
|
# make sure that modules are reloadable
|
|
# when registering
|
|
# handy for development
|
|
# first import dependencies for the method
|
|
if "Font" in locals():
|
|
importlib.reload(Font)
|
|
importlib.reload(utils)
|
|
importlib.reload(butils)
|
|
importlib.reload(bimport)
|
|
importlib.reload(addon_updater_ops)
|
|
else:
|
|
from .common import Font
|
|
from .common import utils
|
|
from . import butils
|
|
from . import bimport
|
|
from . import addon_updater_ops
|
|
|
|
|
|
def getPreferences(context):
|
|
preferences = context.preferences
|
|
return preferences.addons[__name__].preferences
|
|
|
|
|
|
@addon_updater_ops.make_annotations
|
|
class ABC3D_addonPreferences(bpy.types.AddonPreferences):
|
|
"""ABC3D Addon Preferences
|
|
|
|
These are the preferences at Edit/Preferences/Add-ons"""
|
|
|
|
bl_idname = __name__
|
|
|
|
# Addon updater preferences.
|
|
auto_check_update = bpy.props.BoolProperty(
|
|
name="Auto-check for Update",
|
|
description="If enabled, auto-check for updates using an interval",
|
|
default=False)
|
|
|
|
updater_interval_months = bpy.props.IntProperty(
|
|
name='Months',
|
|
description="Number of months between checking for updates",
|
|
default=0,
|
|
min=0)
|
|
|
|
updater_interval_days = bpy.props.IntProperty(
|
|
name='Days',
|
|
description="Number of days between checking for updates",
|
|
default=7,
|
|
min=0,
|
|
max=31)
|
|
|
|
updater_interval_hours = bpy.props.IntProperty(
|
|
name='Hours',
|
|
description="Number of hours between checking for updates",
|
|
default=0,
|
|
min=0,
|
|
max=23)
|
|
|
|
updater_interval_minutes = bpy.props.IntProperty(
|
|
name='Minutes',
|
|
description="Number of minutes between checking for updates",
|
|
default=0,
|
|
min=0,
|
|
max=59)
|
|
|
|
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")
|
|
|
|
# Works best if a column, or even just self.layout.
|
|
mainrow = layout.row()
|
|
col = mainrow.column()
|
|
|
|
# Updater draw function, could also pass in col as third arg.
|
|
addon_updater_ops.update_settings_ui(self, context)
|
|
|
|
|
|
class ABC3D_available_font(bpy.types.PropertyGroup):
|
|
font_name: bpy.props.StringProperty(name="")
|
|
face_name: bpy.props.StringProperty(name="")
|
|
|
|
|
|
class ABC3D_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 ABC3D_text_properties(bpy.types.PropertyGroup):
|
|
|
|
def font_items_callback(self, context):
|
|
items = []
|
|
for f in Font.get_loaded_fonts_and_faces():
|
|
items.append((f"{f[0]} {f[1]}", f"{f[0]} {f[1]}", ""))
|
|
return items
|
|
|
|
def font_default_callback(self, context):
|
|
d = context.scene.abc3d_data
|
|
items = self.font_items_callback(context)
|
|
if len(d.available_fonts) > 0:
|
|
if len(d.available_fonts) > d.active_text_index:
|
|
f = d.available_fonts[d.active_text_index]
|
|
return 0 # f"{f.font_name} {f.face_name}"
|
|
else:
|
|
f = d.available_fonts[0]
|
|
return 0 # f"{f.font_name} {f.face_name}"
|
|
|
|
if type(self.font_name) != type(None) and type(self.face_name) != type(None):
|
|
return 0 # f"{self.font_name} {self.face_name}"
|
|
else:
|
|
return 0 # ""
|
|
|
|
def glyphs_update_callback(self, context):
|
|
butils.prepare_text(self.font_name,
|
|
self.face_name,
|
|
self.text)
|
|
butils.set_text_on_curve(self)
|
|
|
|
def update_callback(self, context):
|
|
butils.set_text_on_curve(self)
|
|
|
|
def font_update_callback(self, context):
|
|
font_name, face_name = self.font.split(" ")
|
|
self["font_name"] = font_name
|
|
self["face_name"] = face_name
|
|
self.glyphs_update_callback(self)
|
|
|
|
text_id: bpy.props.IntProperty()
|
|
font: bpy.props.EnumProperty(
|
|
items=font_items_callback,
|
|
update=font_update_callback,
|
|
)
|
|
font_name: bpy.props.StringProperty(
|
|
update=glyphs_update_callback
|
|
)
|
|
face_name: bpy.props.StringProperty(
|
|
update=glyphs_update_callback
|
|
)
|
|
text_object: bpy.props.PointerProperty(type=bpy.types.Object)
|
|
text: bpy.props.StringProperty(
|
|
update=glyphs_update_callback
|
|
)
|
|
letter_spacing: bpy.props.FloatProperty(
|
|
update=update_callback,
|
|
name="Letter Spacing",
|
|
description="Letter Spacing",
|
|
options={'ANIMATABLE'},
|
|
step=0.01,
|
|
)
|
|
orientation: bpy.props.FloatVectorProperty(
|
|
update=update_callback,
|
|
name="Orientation",
|
|
default=(1.5707963267948966, 0.0, 0.0), # 90 degrees in radians
|
|
subtype='EULER',
|
|
)
|
|
translation: bpy.props.FloatVectorProperty(
|
|
update=update_callback,
|
|
name="Translation",
|
|
default=(0.0, 0.0, 0.0),
|
|
subtype='TRANSLATION',
|
|
)
|
|
font_size: bpy.props.FloatProperty(
|
|
update=update_callback,
|
|
name="Font Size",
|
|
default=1.0,
|
|
subtype='NONE',
|
|
)
|
|
offset: bpy.props.FloatProperty(
|
|
update=update_callback,
|
|
name="Offset",
|
|
default=0.0,
|
|
subtype='NONE',
|
|
)
|
|
compensate_curvature: bpy.props.BoolProperty(
|
|
update=update_callback,
|
|
name="Compensate Curvature",
|
|
description="Fixes curvature spacing issues for simple curves, don't use on curve with tiny loops.",
|
|
default=True,
|
|
)
|
|
ignore_orientation: bpy.props.BoolProperty(
|
|
update=update_callback,
|
|
name="Ignore Curve Orientation",
|
|
default=False,
|
|
)
|
|
distribution_type: bpy.props.StringProperty()
|
|
glyphs: bpy.props.CollectionProperty(type=ABC3D_glyph_properties)
|
|
|
|
# TODO: simply, merge, cut cut cut
|
|
|
|
|
|
class ABC3D_data(bpy.types.PropertyGroup):
|
|
available_fonts: bpy.props.CollectionProperty(
|
|
type=ABC3D_available_font, name="Available fonts")
|
|
|
|
def active_font_index_update(self, context):
|
|
if len(self.available_fonts) <= self.active_font_index:
|
|
self.active_font_index = len(self.available_fonts) - 1
|
|
|
|
active_font_index: bpy.props.IntProperty(
|
|
default=-1,
|
|
update=active_font_index_update,
|
|
)
|
|
available_texts: bpy.props.CollectionProperty(
|
|
type=ABC3D_text_properties, name="Available texts")
|
|
|
|
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)
|
|
|
|
# def font_path_update_callback(self, context):
|
|
# butils.ShowMessageBox("Friendly Reminder", message="do not forget to click on 'Install new font'")
|
|
# bpy.ops.abc3d.install_font()
|
|
|
|
# stupid hack for Mac OS
|
|
font_path: bpy.props.StringProperty(
|
|
name="Font path",
|
|
description="Install a *.glb or *.gltf fontfile from disk",
|
|
default="",
|
|
maxlen=1024,
|
|
# update=font_path_update_callback,
|
|
subtype="FILE_PATH")
|
|
export_dir: bpy.props.StringProperty(
|
|
name="Export Directory",
|
|
description=f"The directory in which we will export fonts.\nIf it is blank, we will export to the addon assets path.\nThis is where the fonts are installed.",
|
|
subtype="DIR_PATH")
|
|
|
|
|
|
class ABC3D_UL_fonts(bpy.types.UIList):
|
|
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
|
|
# avoids renaming the item by accident
|
|
layout.label(text=f"{index}: {item.font_name} {item.face_name}")
|
|
|
|
def invoke(self, context, event):
|
|
pass
|
|
|
|
|
|
class ABC3D_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))
|
|
# avoids renaming the item by accident
|
|
split.label(text=f"{item.text}")
|
|
|
|
def invoke(self, context, event):
|
|
pass
|
|
|
|
|
|
class ABC3D_PT_Panel(bpy.types.Panel):
|
|
bl_label = f"{__name__} panel"
|
|
bl_category = "ABC3D"
|
|
bl_space_type = "VIEW_3D"
|
|
bl_region_type = "UI"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
icon = 'NONE'
|
|
if len(context.scene.abc3d_data.available_fonts) == 0:
|
|
icon = 'ERROR'
|
|
layout.row().label(text='no fonts loaded yet')
|
|
|
|
layout.operator(f"{__name__}.install_font", text='Install new font')
|
|
layout.operator(f"{__name__}.load_installed_fonts",
|
|
text="load installed fonts", icon=icon)
|
|
layout.operator(f"{__name__}.open_asset_directory",
|
|
text="open asset directory", icon='FILEBROWSER')
|
|
|
|
|
|
# class ABC3D_PT_LoadFontPanel(bpy.types.Panel):
|
|
# bl_label = "Install a new font"
|
|
# bl_parent_id = "ABC3D_PT_Panel"
|
|
# bl_category = "ABC3D"
|
|
# bl_space_type = "VIEW_3D"
|
|
# bl_region_type = "UI"
|
|
|
|
# def draw(self, context):
|
|
# layout = self.layout
|
|
# wm = context.window_manager
|
|
# scene = context.scene
|
|
|
|
# abc3d_data = scene.abc3d_data
|
|
|
|
# box = layout.box()
|
|
# box.row().label(text="1. Select fontfile")
|
|
# box.row().prop(context.scene.abc3d_data, "font_path")
|
|
# box.row().label(text="2. Install it:")
|
|
# box.row().operator(f"{__name__}.install_font", text='Install new font')
|
|
|
|
|
|
class ABC3D_PT_FontList(bpy.types.Panel):
|
|
bl_label = "Font List"
|
|
bl_parent_id = "ABC3D_PT_Panel"
|
|
bl_category = "ABC3D"
|
|
bl_space_type = "VIEW_3D"
|
|
bl_region_type = "UI"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
wm = context.window_manager
|
|
scene = context.scene
|
|
|
|
abc3d_data = scene.abc3d_data
|
|
|
|
layout.label(text="Available Fonts")
|
|
layout.template_list("ABC3D_UL_fonts", "", abc3d_data,
|
|
"available_fonts", abc3d_data, "active_font_index")
|
|
if abc3d_data.active_font_index >= 0:
|
|
available_font = abc3d_data.available_fonts[abc3d_data.active_font_index]
|
|
font_name = available_font.font_name
|
|
face_name = available_font.face_name
|
|
available_glyphs = sorted(
|
|
Font.fonts[font_name].faces[face_name].glyphs_in_fontfile)
|
|
loaded_glyphs = sorted(
|
|
Font.fonts[font_name].faces[face_name].loaded_glyphs)
|
|
box = layout.box()
|
|
box.row().label(text=f"Font Name: {font_name}")
|
|
box.row().label(text=f"Face Name: {face_name}")
|
|
n = 16
|
|
n_rows = int(len(available_glyphs) / n)
|
|
box.row().label(text=f"Glyphs:")
|
|
subbox = box.box()
|
|
for i in range(0, n_rows + 1):
|
|
text = ''.join([f"{u}" for ui, u in enumerate(
|
|
available_glyphs) if ui < (i+1) * n and ui >= i * n])
|
|
scale_y = 0.5
|
|
row = subbox.row()
|
|
row.scale_y = scale_y
|
|
row.alignment = 'CENTER'
|
|
row.label(text=text)
|
|
n_rows = int(len(loaded_glyphs) / n)
|
|
box.row().label(text=f"Loaded/Used Glyphs:")
|
|
subbox = box.box()
|
|
for i in range(0, n_rows + 1):
|
|
text = ''.join([f"{u}" for ui, u in enumerate(
|
|
loaded_glyphs) if ui < (i+1) * n and ui >= i * n])
|
|
scale_y = 0.5
|
|
row = subbox.row()
|
|
row.scale_y = scale_y
|
|
row.label(text=text)
|
|
row = layout.row()
|
|
oper = row.operator(f"{__name__}.load_font",
|
|
text='Load all glyphs in memory')
|
|
oper.font_name = font_name
|
|
oper.face_name = face_name
|
|
|
|
|
|
class ABC3D_PT_TextPlacement(bpy.types.Panel):
|
|
bl_label = "Place Text"
|
|
bl_parent_id = "ABC3D_PT_Panel"
|
|
bl_category = "ABC3D"
|
|
bl_space_type = "VIEW_3D"
|
|
bl_region_type = "UI"
|
|
|
|
can_place = False
|
|
|
|
@classmethod
|
|
def poll(self, context):
|
|
if type(context.active_object) != type(None) and context.active_object.type == 'CURVE':
|
|
self.can_place = True
|
|
else:
|
|
self.can_place = False
|
|
return True
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
wm = context.window_manager
|
|
scene = context.scene
|
|
|
|
abc3d_data = scene.abc3d_data
|
|
|
|
placerow = layout.row()
|
|
placerow.enabled = self.can_place
|
|
placerow.operator(f"{__name__}.placetext", text='Place Text')
|
|
if not self.can_place:
|
|
layout.label(text="Cannot place Text.")
|
|
layout.label(text="Select a curve as active object.")
|
|
|
|
|
|
class ABC3D_PT_TextManagement(bpy.types.Panel):
|
|
bl_label = "Text Management"
|
|
bl_parent_id = "ABC3D_PT_Panel"
|
|
bl_category = "ABC3D"
|
|
bl_space_type = "VIEW_3D"
|
|
bl_region_type = "UI"
|
|
bl_options = {"DEFAULT_CLOSED"}
|
|
|
|
# TODO: perhaps this should be done in a periodic timer
|
|
@classmethod
|
|
def poll(self, context):
|
|
scene = context.scene
|
|
abc3d_data = scene.abc3d_data
|
|
# TODO: update available_texts
|
|
|
|
def update():
|
|
if bpy.context.screen.is_animation_playing:
|
|
return
|
|
active_text_index = -1
|
|
remove_list = []
|
|
for i, t in enumerate(abc3d_data.available_texts):
|
|
if type(t.text_object) == type(None):
|
|
remove_list.append(i)
|
|
continue
|
|
remove_me = True
|
|
for c in t.text_object.children:
|
|
if len(c.users_collection) > 0 and (c.get(f"{utils.prefix()}_linked_textobject")) != type(None) and c.get(f"{utils.prefix()}_linked_textobject") == t.text_id:
|
|
remove_me = False
|
|
# not sure how to solve this reliably atm,
|
|
# we need to reassign the glyph, but also get the proper properties from glyph_properties
|
|
# these might be there in t.glyphs, but linked to removed objects
|
|
# or they might be lost
|
|
if type(next((g for g in t.glyphs if type(g.glyph_object) == type(None)), None)) == type(None):
|
|
g = next((g for g in t.glyphs if type(
|
|
g.glyph_object) == type(None)), None)
|
|
# for g in t.glyphs:
|
|
# if type(g) == type(None):
|
|
# print("IS NONE")
|
|
# if type(g.glyph_object) == type(None):
|
|
# print("go IS NONE")
|
|
# else:
|
|
# if g.glyph_object == c:
|
|
# # print(g.glyph_object.name)
|
|
# pass
|
|
|
|
if remove_me:
|
|
remove_list.append(i)
|
|
|
|
for i in remove_list:
|
|
if type(abc3d_data.available_texts[i].text_object) != type(None):
|
|
mom = abc3d_data.available_texts[i].text_object
|
|
|
|
def delif(o, p):
|
|
if p in o:
|
|
del o[p]
|
|
delif(mom, f"{utils.prefix()}_linked_textobject")
|
|
delif(mom, f"{utils.prefix()}_font_name")
|
|
delif(mom, f"{utils.prefix()}_face_name")
|
|
delif(mom, f"{utils.prefix()}_font_size")
|
|
delif(mom, f"{utils.prefix()}_letter_spacing")
|
|
delif(mom, f"{utils.prefix()}_orientation")
|
|
delif(mom, f"{utils.prefix()}_translation")
|
|
delif(mom, f"{utils.prefix()}_offset")
|
|
abc3d_data.available_texts.remove(i)
|
|
|
|
for i, t in enumerate(abc3d_data.available_texts):
|
|
if context.active_object == t.text_object:
|
|
active_text_index = i
|
|
if (hasattr(context.active_object, "parent") and
|
|
context.active_object.parent == t.text_object):
|
|
active_text_index = i
|
|
|
|
if active_text_index != abc3d_data.active_text_index:
|
|
abc3d_data.active_text_index = active_text_index
|
|
|
|
# butils.run_in_main_thread(update)
|
|
|
|
return True
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
wm = context.window_manager
|
|
scene = context.scene
|
|
|
|
abc3d_data = scene.abc3d_data
|
|
|
|
layout.label(text="Text Objects")
|
|
layout.template_list("ABC3D_UL_texts", "", abc3d_data,
|
|
"available_texts", abc3d_data, "active_text_index")
|
|
layout.row().operator(
|
|
f"{__name__}.remove_text", text="Remove Textobject")
|
|
|
|
|
|
class ABC3D_PT_FontCreation(bpy.types.Panel):
|
|
bl_label = "Font Creation"
|
|
bl_parent_id = "ABC3D_PT_Panel"
|
|
bl_category = "ABC3D"
|
|
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
|
|
|
|
abc3d_data = scene.abc3d_data
|
|
|
|
layout.row().operator(
|
|
f"{__name__}.create_font_from_objects", text='Create/Extend Font')
|
|
box = layout.box()
|
|
box.row().label(text="Exporting a fontfile")
|
|
box.row().label(text="1. Select export directory:")
|
|
box.prop(abc3d_data, 'export_dir')
|
|
box.row().label(text="2. More options and export:")
|
|
|
|
box.row().operator(f"{__name__}.save_font_to_file",
|
|
text='Export Font To File')
|
|
layout.row().operator(
|
|
f"{__name__}.toggle_abc3d_collection", text='Toggle Collection')
|
|
box = layout.box()
|
|
box.label(text="metrics")
|
|
box.row().operator(
|
|
f"{__name__}.add_default_metrics", text='Add Default Metrics')
|
|
box.row().operator(f"{__name__}.remove_metrics", text='Remove Metrics')
|
|
box.row().operator(f"{__name__}.align_metrics", text='Align Metrics')
|
|
box.row().operator(
|
|
f"{__name__}.align_metrics_to_active_object", text='Align Metrics to Active Object')
|
|
layout.row().operator(
|
|
f"{__name__}.temporaryhelper", text='Debug Function Do Not Use')
|
|
|
|
|
|
class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel):
|
|
bl_label = "Text Properties"
|
|
bl_parent_id = "ABC3D_PT_TextManagement"
|
|
bl_category = "ABC3D"
|
|
bl_space_type = "VIEW_3D"
|
|
bl_region_type = "UI"
|
|
|
|
def get_active_text_properties(self):
|
|
# and bpy.context.object.select_get():
|
|
if type(bpy.context.active_object) != type(None):
|
|
for t in bpy.context.scene.abc3d_data.available_texts:
|
|
if bpy.context.active_object == t.text_object:
|
|
return t
|
|
if bpy.context.active_object.parent == t.text_object:
|
|
return t
|
|
return None
|
|
|
|
# def font_items_callback(self, context):
|
|
# items = []
|
|
# fonts = Font.get_loaded_fonts_and_faces()
|
|
# for f in fonts:
|
|
# items.append((f"{f[0]} {f[1]}", f"{f[0]} {f[1]}", ""))
|
|
# return items
|
|
|
|
# def font_default_callback(self, context):
|
|
# t = self.get_active_text_properties(self)
|
|
# if type(t) != type(None):
|
|
# return f"{t.font_name} {t.face_name}"
|
|
# else:
|
|
# return None
|
|
|
|
# def font_update_callback(self, context):
|
|
# font_name, face_name = self.font.split(" ")
|
|
# t = self.get_active_text_properties(self)
|
|
# t.font_name = font_name
|
|
# t.face_name = face_name
|
|
# butils.set_text_on_curve(t)
|
|
|
|
# font: bpy.props.EnumProperty(
|
|
# items=font_items_callback,
|
|
# default=font_default_callback,
|
|
# update=font_update_callback,
|
|
# )
|
|
|
|
@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
|
|
abc3d_data = scene.abc3d_data
|
|
|
|
props = self.get_active_text_properties()
|
|
|
|
if type(props) == type(None) or type(props.text_object) == type(None):
|
|
# this should not happen
|
|
# as then polling does not work
|
|
# however, we are paranoid
|
|
return
|
|
|
|
layout.label(text=f"Mom: {props.text_object.name}")
|
|
layout.row().prop(props, "font")
|
|
layout.row().prop(props, "text")
|
|
layout.row().prop(props, "letter_spacing")
|
|
layout.row().prop(props, "font_size")
|
|
layout.row().prop(props, "offset")
|
|
layout.row().prop(props, "compensate_curvature")
|
|
layout.row().prop(props, "ignore_orientation")
|
|
layout.column().prop(props, "translation")
|
|
layout.column().prop(props, "orientation")
|
|
|
|
|
|
class ABC3D_OT_InstallFont(bpy.types.Operator):
|
|
"""Install or load Fontfile from path above.
|
|
(Format must be *.glb or *.gltf)"""
|
|
bl_idname = f"{__name__}.install_font"
|
|
bl_label = "Load Font"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def font_path_update_callback(self, context):
|
|
if os.path.exists(self.font_path):
|
|
print(f"{self.font_path} does exist")
|
|
else:
|
|
print(f"{self.font_path} does not exist")
|
|
|
|
font_path: bpy.props.StringProperty(
|
|
name="Font path",
|
|
description="Install a *.glb or *.gltf fontfile from disk",
|
|
default="",
|
|
maxlen=1024,
|
|
update=font_path_update_callback,
|
|
subtype="FILE_PATH")
|
|
|
|
install_in_assets: bpy.props.BoolProperty(
|
|
name="install in assets",
|
|
description="install the font in the assets directory of the addon",
|
|
default=True,
|
|
)
|
|
|
|
load_into_memory: bpy.props.BoolProperty(name="load font data into memory",
|
|
description="if false, it will load font data on demand",
|
|
default=False)
|
|
|
|
def draw(self, context):
|
|
abc3d_data = context.scene.abc3d_data
|
|
layout = self.layout
|
|
layout.row().prop(self, "font_path")
|
|
# layout.row().prop(abc3d_data, "font_path") # closes the stupid panel on Mac OS..
|
|
layout.row().prop(self, "install_in_assets")
|
|
if not self.install_in_assets and not self.load_into_memory:
|
|
layout.label(text="If the fontfile is not installed,")
|
|
layout.label(
|
|
text="and the font is not loaded in memory completely,")
|
|
layout.label(text="the fontfile should not be moved.")
|
|
layout.row().prop(self, "load_into_memory")
|
|
if self.load_into_memory:
|
|
layout.label(text="Loading font files can take a long time")
|
|
layout.label(text="and use a lot of RAM.")
|
|
layout.label(text="We recommend not doing this and let us")
|
|
layout.label(text="load the font data on demand.")
|
|
|
|
def invoke(self, context, event):
|
|
# self.font_path = butils.bpy_to_abspath(self.font_path)
|
|
# if not os.path.exists(self.font_path):
|
|
# bpy.app.timers.register(lambda: butils.ShowMessageBox(
|
|
# title=f"{__name__} Warning",
|
|
# icon="ERROR",
|
|
# message=[
|
|
# f"We believe the font path ({self.font_path}) does not exist.",
|
|
# f"Did you select your fontfile in the field above the 'Install new font'-button?",
|
|
# ],
|
|
# ), first_interval=0.1)
|
|
return context.window_manager.invoke_props_dialog(self)
|
|
|
|
def execute(self, context):
|
|
scene = bpy.context.scene
|
|
|
|
abc3d_data = context.scene.abc3d_data
|
|
font_path = butils.bpy_to_abspath(self.font_path)
|
|
if not os.path.exists(font_path):
|
|
butils.ShowMessageBox(
|
|
title=f"{__name__} Warning",
|
|
icon="ERROR",
|
|
message=[
|
|
f"Could not install font.",
|
|
f"We believe the font path ({font_path}) does not exist.",
|
|
f"If this is an error, please let us know.",
|
|
],
|
|
)
|
|
return {'CANCELLED'}
|
|
|
|
if self.install_in_assets:
|
|
preferences = getPreferences(context)
|
|
filename = os.path.basename(font_path)
|
|
target = os.path.join(preferences.assets_dir, "fonts", filename)
|
|
import shutil
|
|
os.makedirs(os.path.dirname(target), exist_ok=True)
|
|
shutil.copyfile(font_path, target)
|
|
# def register_load(target, load=False):
|
|
# print(f"registering installed fonts")
|
|
# bpy.app.timers.register(lambda: register_load(target, self.load_into_memory), first_interval=5)
|
|
butils.register_font_from_filepath(target)
|
|
if self.load_into_memory:
|
|
butils.load_font_from_filepath(target)
|
|
butils.update_available_fonts()
|
|
else:
|
|
butils.register_font_from_filepath(font_path)
|
|
if self.load_into_memory:
|
|
butils.load_font_from_filepath(font_path)
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
class ABC3D_OT_OpenAssetDirectory(bpy.types.Operator):
|
|
"""Open Asset Directory"""
|
|
bl_idname = f"{__name__}.open_asset_directory"
|
|
bl_label = "Opens asset directory."
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def execute(self, context):
|
|
preferences = getPreferences(context)
|
|
directory = os.path.realpath(preferences.assets_dir)
|
|
if os.path.exists(directory):
|
|
utils.open_file_browser(directory)
|
|
return {'FINISHED'}
|
|
else:
|
|
butils.ShowMessageBox(
|
|
title=f"{__name__} Warning",
|
|
icon="ERROR",
|
|
message=("Asset directory does not exist.",
|
|
f"Command failed trying to access '{directory}'.",
|
|
"Please, make sure it exists or chose another directory."))
|
|
return {'CANCELLED'}
|
|
|
|
|
|
class ABC3D_OT_LoadInstalledFonts(bpy.types.Operator):
|
|
"""Load installed fontfiles from datapath."""
|
|
bl_idname = f"{__name__}.load_installed_fonts"
|
|
bl_label = "Loading installed Fonts."
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
load_into_memory: bpy.props.BoolProperty(name="load font data into memory",
|
|
description="if false, it will load font data on demand",
|
|
default=False)
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.row().prop(self, "load_into_memory")
|
|
if self.load_into_memory:
|
|
layout.label(text="Loading font files can take a long time")
|
|
layout.label(text="and use a lot of RAM.")
|
|
layout.label(text="We recommend not doing this and let us")
|
|
layout.label(text="load the font data on demand.")
|
|
|
|
def invoke(self, context, event):
|
|
return context.window_manager.invoke_props_dialog(self)
|
|
|
|
def execute(self, context):
|
|
print("EXECUTE LOAD INSTALLED FONTS")
|
|
scene = bpy.context.scene
|
|
|
|
if self.load_into_memory:
|
|
butils.load_installed_fonts()
|
|
else:
|
|
butils.register_installed_fonts()
|
|
butils.ShowMessageBox("Loading Fonts",
|
|
'INFO',
|
|
"Updating Data Structures.")
|
|
butils.update_available_fonts()
|
|
butils.ShowMessageBox("Loading Fonts",
|
|
'INFO',
|
|
"Done loading installed fonts.")
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
class ABC3D_OT_LoadFont(bpy.types.Operator):
|
|
"""Load all glyphs from a specific font in memory.\nThis can take a while and slow down Blender."""
|
|
bl_idname = f"{__name__}.load_font"
|
|
bl_label = "Loading Font."
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
font_name: bpy.props.StringProperty()
|
|
face_name: bpy.props.StringProperty()
|
|
|
|
def execute(self, context):
|
|
filepaths = Font.fonts[self.font_name].faces[self.face_name].filepaths
|
|
for f in filepaths:
|
|
butils.load_font_from_filepath(f)
|
|
return {'FINISHED'}
|
|
|
|
|
|
class ABC3D_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 ABC3D_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 ABC3D_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 ABC3D_OT_AlignMetrics(bpy.types.Operator):
|
|
"""Align metrics of selected objects to each other"""
|
|
bl_idname = f"{__name__}.align_metrics"
|
|
bl_label = "Align metrics"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def execute(self, context):
|
|
objects = bpy.context.selected_objects
|
|
butils.align_metrics_of_objects(objects)
|
|
return {'FINISHED'}
|
|
|
|
|
|
class ABC3D_OT_TemporaryHelper(bpy.types.Operator):
|
|
"""Temporary Helper ABC3D\nThis could do anything.\nIt's just there to make random functions available for testing."""
|
|
bl_idname = f"{__name__}.temporaryhelper"
|
|
bl_label = "Temp Font"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def execute(self, context):
|
|
global shared
|
|
scene = bpy.context.scene
|
|
abc3d_data = scene.abc3d_data
|
|
|
|
# butils.load_font_from_filepath("/home/jrkb/.config/blender/4.1/datafiles/abc3d/fonts/NM_Origin.glb")
|
|
butils.update_available_fonts()
|
|
|
|
# objects = bpy.context.selected_objects
|
|
# butils.add_default_metrics_to_objects(objects)
|
|
# reference_bound_box = None
|
|
# for o in objects:
|
|
# bb = o.bound_box
|
|
# reference_bound_box = butils.get_max_bound_box(bb, reference_bound_box)
|
|
# for o in objects:
|
|
# metrics = butils.get_metrics_bound_box(o.bound_box, reference_bound_box)
|
|
# butils.add_metrics_obj_from_bound_box(o, metrics)
|
|
# bpy.app.timers.register(lambda: butils.remove_metrics_from_objects(objects), first_interval=5)
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
class ABC3D_OT_RemoveText(bpy.types.Operator):
|
|
"""Remove Text 3D"""
|
|
bl_idname = f"{__name__}.remove_text"
|
|
bl_label = "Remove Text"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
remove_objects: bpy.props.BoolProperty(
|
|
name="Remove Objects",
|
|
description="Remove both ABC3D text functionality and the objects/meshes",
|
|
default=True)
|
|
|
|
def invoke(self, context, event):
|
|
wm = context.window_manager
|
|
return wm.invoke_props_dialog(self)
|
|
|
|
def execute(self, context):
|
|
abc3d_data = context.scene.abc3d_data
|
|
if abc3d_data.active_text_index < 0:
|
|
butils.ShowMessageBox(
|
|
title="No text selected",
|
|
message=("Please select a text."),
|
|
icon='GHOST_ENABLED')
|
|
return {'CANCELLED'}
|
|
|
|
i = abc3d_data.active_text_index
|
|
if type(abc3d_data.available_texts[i].text_object) != type(None):
|
|
mom = abc3d_data.available_texts[i].text_object
|
|
|
|
def delif(o, p):
|
|
if p in o:
|
|
del o[p]
|
|
delif(mom, f"{utils.prefix()}_linked_textobject")
|
|
delif(mom, f"{utils.prefix()}_font_name")
|
|
delif(mom, f"{utils.prefix()}_face_name")
|
|
delif(mom, f"{utils.prefix()}_font_size")
|
|
delif(mom, f"{utils.prefix()}_letter_spacing")
|
|
delif(mom, f"{utils.prefix()}_orientation")
|
|
delif(mom, f"{utils.prefix()}_translation")
|
|
delif(mom, f"{utils.prefix()}_offset")
|
|
if self.remove_objects:
|
|
remove_list = []
|
|
for g in abc3d_data.available_texts[i].glyphs:
|
|
if type(g) != type(None):
|
|
remove_list.append(g.glyph_object)
|
|
butils.simply_delete_objects(remove_list)
|
|
|
|
abc3d_data.available_texts.remove(i)
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
class ABC3D_OT_PlaceText(bpy.types.Operator):
|
|
"""Place Text 3D on active object"""
|
|
bl_idname = f"{__name__}.placetext"
|
|
bl_label = "Place Text"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def font_items_callback(self, context):
|
|
items = []
|
|
fonts = Font.get_loaded_fonts_and_faces()
|
|
for f in fonts:
|
|
items.append((f"{f[0]} {f[1]}", f"{f[0]} {f[1]}", ""))
|
|
return items
|
|
|
|
def font_update_callback(self, context):
|
|
font_name, face_name = self.font.split(" ")
|
|
self.font_name = font_name
|
|
self.face_name = face_name
|
|
|
|
font_name: bpy.props.StringProperty(
|
|
options={'HIDDEN'}
|
|
)
|
|
face_name: bpy.props.StringProperty(
|
|
options={'HIDDEN'}
|
|
)
|
|
font: bpy.props.EnumProperty(items=font_items_callback,
|
|
update=font_update_callback
|
|
)
|
|
text: bpy.props.StringProperty(
|
|
name="Text",
|
|
description="The text.",
|
|
default="ABC3D",
|
|
maxlen=1024,
|
|
)
|
|
# target_object: bpy.props.PointerProperty(
|
|
# name="The Target Object",
|
|
# description="The target, which will be populated by character children of text.",
|
|
# type=bpy.types.Object,
|
|
# )
|
|
letter_spacing: bpy.props.FloatProperty(
|
|
name="Letter Spacing",
|
|
description="Letter Spacing",
|
|
default=0.0,
|
|
)
|
|
font_size: bpy.props.FloatProperty(
|
|
name="Font Size",
|
|
default=1.0,
|
|
subtype='NONE',
|
|
)
|
|
offset: bpy.props.FloatProperty(
|
|
name="Offset",
|
|
default=0.0,
|
|
subtype='NONE',
|
|
)
|
|
translation: bpy.props.FloatVectorProperty(
|
|
name="Translation",
|
|
default=(0.0, 0.0, 0.0),
|
|
subtype='TRANSLATION',
|
|
)
|
|
orientation: bpy.props.FloatVectorProperty(
|
|
name="Orientation",
|
|
default=(1.5707963267948966, 0.0, 0.0), # 90 degrees in radians
|
|
subtype='EULER',
|
|
)
|
|
|
|
def invoke(self, context, event):
|
|
wm = context.window_manager
|
|
self.font_update_callback(context)
|
|
return wm.invoke_props_dialog(self)
|
|
|
|
def execute(self, context):
|
|
global shared
|
|
scene = bpy.context.scene
|
|
|
|
abc3d_data = scene.abc3d_data
|
|
|
|
selected = bpy.context.view_layer.objects.active
|
|
|
|
if selected:
|
|
# font = abc3d_data.available_fonts[abc3d_data.active_font_index]
|
|
# font_name = font.font_name
|
|
# face_name = font.face_name
|
|
|
|
distribution_type = 'DEFAULT'
|
|
|
|
text_id = 0
|
|
for i, tt in enumerate(abc3d_data.available_texts):
|
|
while text_id == tt.text_id:
|
|
text_id = text_id + 1
|
|
t = abc3d_data.available_texts.add()
|
|
# If you wish to set a value and not fire an update, set the id property.
|
|
# A property defined via bpy.props for example ob.prop is stored as ob["prop"] once set to non default.
|
|
t['text_id'] = text_id
|
|
# t['font'] = self.font # enums want to be set as attribute
|
|
t['font_name'] = self.font_name
|
|
t['face_name'] = self.face_name
|
|
t.text_object = selected
|
|
t['text'] = self.text
|
|
t['letter_spacing'] = self.letter_spacing
|
|
t['font_size'] = self.font_size
|
|
t['offset'] = self.offset
|
|
t['translation'] = self.translation
|
|
t['orientation'] = self.orientation
|
|
t['distribution_type'] = distribution_type
|
|
t.font = self.font # enums want to be set as attribute
|
|
# this also calls the update function
|
|
# so we don't need to prepare/set again
|
|
# no need for these:
|
|
# butils.prepare_text(t.font_name,
|
|
# t.face_name,
|
|
# t.text)
|
|
# or this:
|
|
# butils.set_text_on_curve(t)
|
|
# else:
|
|
butils.ShowMessageBox(
|
|
title="No object selected",
|
|
message=(
|
|
"Please select an object.",
|
|
"It will be used to put the type on.",
|
|
"Thank you :)"),
|
|
icon='GHOST_ENABLED')
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
class ABC3D_OT_ToggleABC3DCollection(bpy.types.Operator):
|
|
"""Toggle ABC3D Collection"""
|
|
bl_idname = f"{__name__}.toggle_abc3d_collection"
|
|
bl_label = "Toggle Collection visibility"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def execute(self, context):
|
|
scene = context.scene
|
|
fontcollection = bpy.data.collections.get("ABC3D")
|
|
|
|
if fontcollection is None:
|
|
self.report(
|
|
{'INFO'}, f"{bl_info['name']}: There is no collection. Did you use or create any glyphs yet?")
|
|
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 ABC3D_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 invoke(self, context, event):
|
|
wm = context.window_manager
|
|
preferences = getPreferences(context)
|
|
abc3d_data = context.scene.abc3d_data
|
|
if abc3d_data.export_dir == "":
|
|
abc3d_data.export_dir = os.path.join(
|
|
preferences.assets_dir, "fonts")
|
|
return wm.invoke_props_dialog(self)
|
|
|
|
def draw(self, context):
|
|
abc3d_data = context.scene.abc3d_data
|
|
layout = self.layout
|
|
layout.label(text="Available Fonts")
|
|
layout.template_list("ABC3D_UL_fonts", "", abc3d_data,
|
|
"available_fonts", abc3d_data, "active_font_index")
|
|
available_font = abc3d_data.available_fonts[abc3d_data.active_font_index]
|
|
font_name = available_font.font_name
|
|
face_name = available_font.face_name
|
|
loaded_glyphs = sorted(
|
|
Font.fonts[font_name].faces[face_name].loaded_glyphs)
|
|
n = 16
|
|
n_rows = int(len(loaded_glyphs) / n)
|
|
box = layout.box()
|
|
box.row().label(text=f"Glyphs to be exported:")
|
|
subbox = box.box()
|
|
for i in range(0, n_rows + 1):
|
|
text = ''.join([f"{u}" for ui, u in enumerate(
|
|
loaded_glyphs) if ui < (i+1) * n and ui >= i * n])
|
|
scale_y = 0.5
|
|
row = subbox.row()
|
|
row.scale_y = scale_y
|
|
row.label(text=text)
|
|
layout.prop(abc3d_data, 'export_dir')
|
|
|
|
def execute(self, context):
|
|
global shared
|
|
scene = bpy.context.scene
|
|
abc3d_data = scene.abc3d_data
|
|
|
|
fontcollection = bpy.data.collections.get("ABC3D")
|
|
|
|
# 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 abc3d_data.active_font_index < 0:
|
|
self.report(
|
|
{'INFO'}, f"{bl_info['name']}: There is no active font")
|
|
return {'CANCELLED'}
|
|
|
|
if len(abc3d_data.available_fonts) <= abc3d_data.active_font_index:
|
|
self.report(
|
|
{'INFO'}, f"{bl_info['name']}: Active font is not available")
|
|
return {'CANCELLED'}
|
|
|
|
# save state to restore later
|
|
was_fontcollection_linked = scene.collection.children.find(
|
|
fontcollection.name) >= 0
|
|
was_selection = []
|
|
for obj in bpy.context.selected_objects:
|
|
was_selection.append(obj)
|
|
was_active_object = bpy.context.view_layer.objects.active
|
|
|
|
bpy.ops.object.select_all(action="DESELECT")
|
|
|
|
# get save data
|
|
selected_font = abc3d_data.available_fonts[abc3d_data.active_font_index]
|
|
|
|
# print(selected_font.font_name)
|
|
self.report(
|
|
{'INFO'}, f"{bl_info['name']}: {selected_font.font_name} {selected_font.face_name}")
|
|
preferences = getPreferences(context)
|
|
print(f"assets folder: {preferences.assets_dir}")
|
|
|
|
bpy.ops.scene.new(type='FULL_COPY')
|
|
|
|
linked_collections = bpy.context.scene.collection.children.values()
|
|
for c in linked_collections:
|
|
bpy.context.scene.collection.children.unlink(c)
|
|
bpy.context.scene.collection.children.link(fontcollection)
|
|
|
|
# select what needs to be selected
|
|
export_objects = []
|
|
for obj in fontcollection.objects:
|
|
if obj["font_name"] == selected_font.font_name:
|
|
if not butils.is_metrics_object(obj):
|
|
obj.select_set(True)
|
|
export_objects.append(obj)
|
|
else:
|
|
obj.select_set(True)
|
|
butils.add_faces_to_metrics(obj)
|
|
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"{abc3d_data.export_dir}/{selected_font.font_name}.glb"
|
|
# get rid of scene extra data before export
|
|
scene_keys = []
|
|
for k in bpy.context.scene.keys():
|
|
scene_keys.append(k)
|
|
for k in scene_keys:
|
|
del bpy.context.scene[k]
|
|
|
|
# save as gltf
|
|
bpy.ops.export_scene.gltf(
|
|
filepath=filepath,
|
|
check_existing=False,
|
|
# GLB or GLTF_SEPARATE (also change filepath)
|
|
export_format='GLB',
|
|
export_extras=True,
|
|
use_selection=True,
|
|
use_active_scene=True,
|
|
)
|
|
bpy.app.timers.register(
|
|
lambda: bpy.ops.scene.delete(), first_interval=1)
|
|
|
|
# bpy.ops.scene.delete()
|
|
# restore()
|
|
|
|
def remove_faces():
|
|
for obj in fontcollection.objects:
|
|
if obj["font_name"] == selected_font.font_name:
|
|
if butils.is_metrics_object(obj):
|
|
butils.remove_faces_from_metrics(obj)
|
|
bpy.app.timers.register(lambda: remove_faces(), first_interval=2)
|
|
self.report({'INFO'}, f"did it")
|
|
|
|
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', 'abc3d']
|
|
# for addon in keep:
|
|
# bpy.ops.preferences.addon_enable(module=addon)
|
|
|
|
|
|
class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator):
|
|
"""Create Font from selected objects"""
|
|
bl_idname = f"{__name__}.create_font_from_objects"
|
|
bl_label = "Create Font"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
font_name: bpy.props.StringProperty(
|
|
default="NM_Origin",
|
|
)
|
|
face_name: bpy.props.StringProperty(
|
|
default="Tender",
|
|
)
|
|
import_infix: bpy.props.StringProperty(
|
|
default="_NM_Origin_Tender",
|
|
)
|
|
autodetect_names: bpy.props.BoolProperty(
|
|
default=True,
|
|
)
|
|
fix_common_misspellings: bpy.props.BoolProperty(
|
|
default=True,
|
|
)
|
|
|
|
def invoke(self, context, event):
|
|
wm = context.window_manager
|
|
return wm.invoke_props_dialog(self)
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
if len(context.selected_objects) == 0:
|
|
layout.row().label(text="No objects selected.", icon="ERROR")
|
|
layout.row().label(text="Please select your glyphs first.", icon="INFO")
|
|
else:
|
|
row = layout.row()
|
|
row.prop(self, 'autodetect_names')
|
|
if self.autodetect_names:
|
|
scale_y = 0.5
|
|
row = layout.row()
|
|
row.scale_y = scale_y
|
|
row.label(
|
|
text="Watch out, follow convention in naming your meshes:")
|
|
row = layout.row()
|
|
row.scale_y = scale_y
|
|
row.label(text="'<glyph id>_<font name>_<face name>'")
|
|
row = layout.row()
|
|
row.scale_y = scale_y
|
|
row.label(text=" - glyph id: unicode glyph name or raw glyph")
|
|
row = layout.row()
|
|
row.scale_y = scale_y
|
|
row.label(text=" - font name: font name with underscore")
|
|
row = layout.row()
|
|
row.scale_y = scale_y
|
|
row.label(text=" - face name: face name")
|
|
row = layout.row()
|
|
row.scale_y = scale_y
|
|
row.label(text="working examples:")
|
|
row = layout.row()
|
|
row.scale_y = scale_y
|
|
row.label(text="- 'A_NM_Origin_Tender'")
|
|
row = layout.row()
|
|
row.scale_y = scale_y
|
|
row.label(text="- 'B_NM_Origin_Tender'")
|
|
row = layout.row()
|
|
row.scale_y = scale_y
|
|
row.label(text="- 'arrowright_NM_Origin_Tender'")
|
|
row = layout.row()
|
|
row.scale_y = scale_y
|
|
row.label(text="- '→_NM_Origin_Tender' (equal to above)")
|
|
row = layout.row()
|
|
row.scale_y = scale_y
|
|
row.label(text="- 'quotesingle_NM_Origin_Tender.001'")
|
|
row = layout.row()
|
|
row.scale_y = scale_y
|
|
row.label(text="- 'colon_NM_Origin_Tender_2'")
|
|
box = layout.box()
|
|
box.enabled = not self.autodetect_names
|
|
box.prop(self, 'font_name')
|
|
box.prop(self, 'face_name')
|
|
box.prop(self, 'import_infix')
|
|
layout.prop(self, 'fix_common_misspellings')
|
|
if self.fix_common_misspellings:
|
|
for k in Font.known_misspellings:
|
|
character = ""
|
|
if Font.known_misspellings[k] in Font.name_to_glyph_d:
|
|
character = f" ({Font.name_to_glyph_d[Font.known_misspellings[k]]})"
|
|
row = layout.row()
|
|
row.scale_y = 0.5
|
|
row.label(
|
|
text=f"{k} → {Font.known_misspellings[k]}{character}")
|
|
|
|
def execute(self, context):
|
|
print(f"executing {self.bl_idname}")
|
|
if len(context.selected_objects) == 0:
|
|
print(f"cancelled {self.bl_idname} - no objects selected")
|
|
return {'CANCELLED'}
|
|
global shared
|
|
scene = bpy.context.scene
|
|
abc3d_data = scene.abc3d_data
|
|
|
|
fontcollection = bpy.data.collections.get("ABC3D")
|
|
|
|
if fontcollection is None:
|
|
fontcollection = bpy.data.collections.new("ABC3D")
|
|
|
|
ifxsplit = self.import_infix.split('_')
|
|
# if len(ifxsplit) != 4:
|
|
|
|
# font_name = f"{ifxsplit[1]}_{ifxsplit[2]}"
|
|
# face_name = ifxsplit[3]
|
|
font_name = self.font_name
|
|
face_name = self.face_name
|
|
|
|
# TODO: do not clear
|
|
# abc3d_data.available_fonts.clear()
|
|
# Font.fonts = {}
|
|
currentObjects = []
|
|
for o in context.selected_objects:
|
|
if o.name not in currentObjects:
|
|
print(f"processing {o.name}")
|
|
process_object = True
|
|
if self.autodetect_names:
|
|
ifxsplit = o.name.split('_')
|
|
if len(ifxsplit) < 4:
|
|
print(
|
|
f"whoops name could not be autodetected {o.name}")
|
|
continue
|
|
font_name = f"{ifxsplit[1]}_{ifxsplit[2]}"
|
|
face_name = ifxsplit[3]
|
|
|
|
if butils.is_mesh(o) and not butils.is_metrics_object(o):
|
|
uc = o.users_collection
|
|
if self.fix_common_misspellings:
|
|
o.name = Font.fix_glyph_name_misspellings(o.name)
|
|
# name = re.sub(regex, "", o.name)
|
|
# glyph_id = Font.name_to_glyph(name)
|
|
name = o.name.split('_')[0]
|
|
glyph_id = Font.name_to_glyph(name)
|
|
|
|
if type(glyph_id) != type(None):
|
|
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)
|
|
|
|
# TODO: is there a better way to iterate over a CollectionProperty?
|
|
found = False
|
|
for f in abc3d_data.available_fonts.values():
|
|
if (f.font_name == font_name
|
|
and f.face_name == face_name):
|
|
found = True
|
|
break
|
|
if not found:
|
|
f = abc3d_data.available_fonts.add()
|
|
f.font_name = font_name
|
|
f.face_name = face_name
|
|
|
|
else:
|
|
print(
|
|
f"import warning: did not understand glyph {name}")
|
|
self.report(
|
|
{'INFO'}, f"did not understand glyph {name}")
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
class ABC3D_PT_RightPropertiesPanel(bpy.types.Panel):
|
|
"""Creates a Panel in the Object properties window"""
|
|
bl_label = f"{bl_info['name']}"
|
|
bl_idname = "ABC3D_PT_RightPropertiesPanel"
|
|
bl_space_type = 'PROPERTIES'
|
|
bl_region_type = 'WINDOW'
|
|
bl_context = "object"
|
|
|
|
@classmethod
|
|
def poll(self, context):
|
|
# only show the panel, if it's a textobject or a glyph
|
|
is_text = type(next((t for t in context.scene.abc3d_data.available_texts if t.text_object ==
|
|
context.active_object), None)) != type(None)
|
|
is_glyph = type(next((t for t in context.scene.abc3d_data.available_texts if t.text_object ==
|
|
context.active_object.parent), None)) != type(None)
|
|
return is_text or is_glyph
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
scene = context.scene
|
|
abc3d_data = scene.abc3d_data
|
|
|
|
obj = context.active_object
|
|
|
|
def is_it_text():
|
|
return type(next((t for t in context.scene.abc3d_data.available_texts if t.text_object == context.active_object), None)) != type(None)
|
|
|
|
def is_it_glyph():
|
|
return type(next((t for t in context.scene.abc3d_data.available_texts if t.text_object == context.active_object.parent), None)) != type(None)
|
|
|
|
is_text = is_it_text()
|
|
is_glyph = is_it_glyph()
|
|
|
|
textobject = obj if is_text else obj.parent if is_glyph else obj
|
|
available_text = abc3d_data.available_texts[abc3d_data.active_text_index]
|
|
|
|
# row = layout.row()
|
|
# row.label(text="Hello world!", icon='WORLD_DATA')
|
|
# row = layout.row()
|
|
# row.label(text="Active object is: " + obj.name)
|
|
# row = layout.row()
|
|
# row.label(text="text object is: " + textobject.name)
|
|
row = layout.row()
|
|
row.label(text=f"active text index is: {abc3d_data.active_text_index}")
|
|
|
|
layout.row().label(text="Text Properties:")
|
|
layout.row().prop(available_text, "text")
|
|
layout.row().prop(available_text, "letter_spacing")
|
|
layout.row().prop(available_text, "font_size")
|
|
layout.row().prop(available_text, "offset")
|
|
layout.row().prop(available_text, "compensate_curvature")
|
|
layout.row().prop(available_text, "ignore_orientation")
|
|
layout.column().prop(available_text, "translation")
|
|
layout.column().prop(available_text, "orientation")
|
|
|
|
if is_glyph:
|
|
layout.row().label(text="Glyph Properties:")
|
|
|
|
|
|
class ABC3D_OT_Reporter(bpy.types.Operator):
|
|
bl_idname = f"{__name__}.reporter"
|
|
bl_label = "Report"
|
|
|
|
label = bpy.props.StringProperty(
|
|
name="label",
|
|
default="INFO",
|
|
)
|
|
message = bpy.props.StringProperty(
|
|
name="message",
|
|
default="I have nothing to say really",
|
|
)
|
|
|
|
def execute(self, context):
|
|
# this is where I send the message
|
|
self.report({'INFO'}, 'whatever')
|
|
for i in range(0, 10):
|
|
butils.ShowMessageBox('whatever', 'INFO', 'INFO')
|
|
return {'FINISHED'}
|
|
|
|
|
|
classes = (
|
|
bimport.ImportGLTF2,
|
|
bimport.GetFontFacesInFile,
|
|
ABC3D_addonPreferences,
|
|
ABC3D_available_font,
|
|
ABC3D_glyph_properties,
|
|
ABC3D_text_properties,
|
|
ABC3D_data,
|
|
ABC3D_UL_fonts,
|
|
ABC3D_UL_texts,
|
|
ABC3D_PT_Panel,
|
|
# ABC3D_PT_LoadFontPanel,
|
|
ABC3D_PT_FontList,
|
|
ABC3D_PT_TextPlacement,
|
|
ABC3D_PT_TextManagement,
|
|
ABC3D_PT_FontCreation,
|
|
ABC3D_PT_TextPropertiesPanel,
|
|
ABC3D_OT_OpenAssetDirectory,
|
|
ABC3D_OT_LoadInstalledFonts,
|
|
ABC3D_OT_LoadFont,
|
|
ABC3D_OT_AddDefaultMetrics,
|
|
ABC3D_OT_RemoveMetrics,
|
|
ABC3D_OT_AlignMetricsToActiveObject,
|
|
ABC3D_OT_AlignMetrics,
|
|
ABC3D_OT_TemporaryHelper,
|
|
ABC3D_OT_RemoveText,
|
|
ABC3D_OT_PlaceText,
|
|
ABC3D_OT_InstallFont,
|
|
ABC3D_OT_ToggleABC3DCollection,
|
|
ABC3D_OT_SaveFontToFile,
|
|
ABC3D_OT_CreateFontFromObjects,
|
|
ABC3D_PT_RightPropertiesPanel,
|
|
ABC3D_OT_Reporter,
|
|
)
|
|
|
|
|
|
def compare_text_object_with_object(t, o, strict=False):
|
|
for k in o.keys():
|
|
if k == f"{utils.prefix()}_type":
|
|
if o[k] != "textobject":
|
|
return False
|
|
elif k.startswith(f"{utils.prefix()}_"):
|
|
p = k.replace(f"{utils.prefix()}_", "")
|
|
if p in t.keys():
|
|
if t[p] != o[k]:
|
|
return False
|
|
else:
|
|
print(f"{__name__} set_text_object: did not find key ({p})")
|
|
if strict:
|
|
return False
|
|
# for p in t.keys():
|
|
# if
|
|
return True
|
|
|
|
|
|
def detect_text():
|
|
scene = bpy.context.scene
|
|
abc3d_data = scene.abc3d_data
|
|
for o in scene.objects:
|
|
if o[f"{utils.prefix()}_type"] == "textobject":
|
|
linked_textobject = int(o[f"{utils.prefix()}_linked_textobject"])
|
|
if len(abc3d_data.available_texts) > linked_textobject \
|
|
and abc3d_data.available_texts[linked_textobject].text_object == o:
|
|
t = abc3d_data.available_texts[linked_textobject]
|
|
a = test_availability(
|
|
o["font_name"], o["face_name"], o["text"])
|
|
butils.transfer_blender_object_to_text_properties(o, t)
|
|
|
|
|
|
def load_used_glyphs():
|
|
print("LOAD USED GLYPHS")
|
|
scene = bpy.context.scene
|
|
abc3d_data = scene.abc3d_data
|
|
for t in abc3d_data.available_texts:
|
|
a = Font.test_availability(t.font_name,
|
|
t.face_name,
|
|
t.text)
|
|
if type(a) == type(int()):
|
|
if a == Font.MISSING_FONT:
|
|
butils.ShowMessageBox("Missing Font",
|
|
"ERROR",
|
|
[f"Font {t.font_name} is missing.",
|
|
"Do you have it installed?"])
|
|
if a is Font.MISSING_FACE:
|
|
butils.ShowMessageBox("Missing FontFace",
|
|
"ERROR",
|
|
[f"Font {t.font_name} is there,",
|
|
f"but the FontFace {t.face_name} is missing,",
|
|
"Do you have it installed?"])
|
|
elif len(a["maybe"]) > 0:
|
|
for fp in a["filepaths"]:
|
|
butils.load_font_from_filepath(fp, a["maybe"])
|
|
|
|
|
|
@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)
|
|
butils.run_in_main_thread(butils.update_available_fonts)
|
|
butils.run_in_main_thread(bpy.ops.abc3d.load_installed_fonts)
|
|
butils.run_in_main_thread(load_used_glyphs)
|
|
|
|
|
|
def load_handler_unload():
|
|
if bpy.app.timers.is_registered(butils.execute_queued_functions):
|
|
bpy.app.timers.unregister(butils.execute_queued_functions)
|
|
|
|
|
|
@persistent
|
|
def on_frame_changed(self, dummy):
|
|
for t in bpy.context.scene.abc3d_data.available_texts:
|
|
# TODO PERFORMANCE: only on demand
|
|
butils.set_text_on_curve(t)
|
|
|
|
@persistent
|
|
def on_depsgraph_update(scene, depsgraph):
|
|
if not bpy.context.mode.startswith("EDIT"):
|
|
for u in depsgraph.updates:
|
|
if f"{utils.prefix()}_linked_textobject" in u.id.keys() \
|
|
and f"{utils.prefix()}_type" in u.id.keys() \
|
|
and u.id[f"{utils.prefix()}_type"] == 'textobject':
|
|
linked_textobject = u.id[f"{utils.prefix()}_linked_textobject"]
|
|
if u.is_updated_geometry and len(scene.abc3d_data.available_texts) > linked_textobject:
|
|
def later():
|
|
if not "lock_depsgraph_update_ntimes" in scene.abc3d_data \
|
|
or scene.abc3d_data["lock_depsgraph_update_ntimes"] == 0:
|
|
print("******* not yet")
|
|
butils.set_text_on_curve(
|
|
scene.abc3d_data.available_texts[linked_textobject])
|
|
elif scene.abc3d_data["lock_depsgraph_update_ntimes"] > 0:
|
|
scene.abc3d_data['lock_depsgraph_update_ntimes'] -= 1
|
|
butils.run_in_main_thread(later)
|
|
|
|
|
|
|
|
def register():
|
|
addon_updater_ops.register(bl_info)
|
|
|
|
for cls in classes:
|
|
addon_updater_ops.make_annotations(cls) # Avoid blender 2.8 warnings.
|
|
bpy.utils.register_class(cls)
|
|
bpy.types.Scene.abc3d_data = bpy.props.PointerProperty(type=ABC3D_data)
|
|
# bpy.types.Object.__del__ = lambda self: print(f"Bye {self.name}")
|
|
print(f"REGISTER {bl_info['name']}")
|
|
|
|
# auto start if we load a blend file
|
|
if load_handler not in bpy.app.handlers.load_post:
|
|
bpy.app.handlers.load_post.append(load_handler)
|
|
# and autostart if we reload script
|
|
load_handler(None, None)
|
|
|
|
if on_frame_changed not in bpy.app.handlers.frame_change_post:
|
|
bpy.app.handlers.frame_change_post.append(on_frame_changed)
|
|
|
|
if on_depsgraph_update not in bpy.app.handlers.depsgraph_update_post:
|
|
bpy.app.handlers.depsgraph_update_post.append(on_depsgraph_update)
|
|
|
|
butils.run_in_main_thread(butils.clear_available_fonts)
|
|
# butils.run_in_main_thread(butils.load_installed_fonts)
|
|
butils.run_in_main_thread(butils.update_available_fonts)
|
|
butils.run_in_main_thread(butils.update_types)
|
|
|
|
# bpy.ops.abc3d.load_installed_fonts()
|
|
|
|
Font.name_to_glyph_d = Font.generate_name_to_glyph_d()
|
|
|
|
|
|
def unregister():
|
|
addon_updater_ops.unregister()
|
|
|
|
for cls in reversed(classes):
|
|
bpy.utils.unregister_class(cls)
|
|
|
|
# remove autostart when loading blend file
|
|
if load_handler in bpy.app.handlers.load_post:
|
|
bpy.app.handlers.load_post.remove(load_handler)
|
|
# and when reload script
|
|
load_handler_unload()
|
|
|
|
if on_frame_changed in bpy.app.handlers.frame_change_post:
|
|
bpy.app.handlers.frame_change_post.remove(on_frame_changed)
|
|
|
|
del bpy.types.Scene.abc3d_data
|
|
print(f"UNREGISTER {bl_info['name']}")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
register()
|