1731 lines
60 KiB
Python
1731 lines
60 KiB
Python
# SPDX-License-Identifier: GPL-2.0-only
|
|
|
|
"""
|
|
A 3D font helper
|
|
"""
|
|
|
|
import functools
|
|
import importlib
|
|
import io
|
|
import os
|
|
|
|
import bpy
|
|
from bpy.app.handlers import persistent
|
|
from bpy.types import Panel
|
|
|
|
bl_info = {
|
|
"name": "ABC3D",
|
|
"author": "Jakob Schlötter, Studio Pointer*",
|
|
"version": (0, 0, 5),
|
|
"blender": (4, 1, 0),
|
|
"location": "VIEW3D",
|
|
"description": "Convenience addon for 3D fonts",
|
|
"category": "Typography",
|
|
}
|
|
# NOTE: also change version in common/utils.py
|
|
|
|
from . import addon_updater_ops, bimport, butils
|
|
from .common import Font, utils
|
|
|
|
# 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)
|
|
|
|
|
|
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)
|
|
|
|
|
|
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_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_lf = row.operator(
|
|
f"{__name__}.load_font", text="Load all glyphs in memory"
|
|
)
|
|
oper_lf.font_name = font_name
|
|
oper_lf.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__}.toggle_abc3d_collection", text="Toggle Collection"
|
|
)
|
|
layout.row().operator(
|
|
f"{__name__}.create_font_from_objects", text="Create/Extend Font"
|
|
)
|
|
layout.row().operator(f"{__name__}.save_font_to_file", text="Export Font To File")
|
|
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):
|
|
font_path = butils.bpy_to_abspath(self.font_path)
|
|
if os.path.exists(font_path):
|
|
print(f"font_path_update: {font_path} does exist")
|
|
else:
|
|
print(f"font_path_update: {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.
|
|
|
|
The metrics of the active object are not changed and is taken as a reference for all other objects.
|
|
"""
|
|
|
|
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.
|
|
|
|
The metrics of all objects are merged and expanded to fit for all objects."""
|
|
|
|
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()}_type")
|
|
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",
|
|
)
|
|
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 do_autodetect_names(self, name: str):
|
|
ifxsplit = name.split("_")
|
|
if len(ifxsplit) < 4:
|
|
print(f"name could not be autodetected {name}")
|
|
print("split:")
|
|
print(ifxsplit)
|
|
return self.font_name, self.face_name
|
|
detected_font_name = f"{ifxsplit[1]}_{ifxsplit[2]}"
|
|
detected_face_name = ifxsplit[3]
|
|
return detected_font_name, detected_face_name
|
|
|
|
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")
|
|
first_object_name = context.selected_objects[-1].name
|
|
self.font_name, self.face_name = (
|
|
self.do_autodetect_names(first_object_name)
|
|
)
|
|
if self.autodetect_names:
|
|
scale_y = 0.5
|
|
row = layout.row()
|
|
row.scale_y = scale_y
|
|
row.label(text="Autodetecting names per glyph.")
|
|
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")
|
|
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")
|
|
|
|
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:
|
|
font_name, face_name, input_infix = self.do_autodetect_names(o.name)
|
|
|
|
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, bpy.types.PointerProperty(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_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_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_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():
|
|
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)
|
|
|
|
|
|
depsgraph_updates_locked = False
|
|
|
|
|
|
def unlock_depsgraph_updates():
|
|
global depsgraph_updates_locked
|
|
depsgraph_updates_locked = False
|
|
|
|
|
|
def lock_depsgraph_updates():
|
|
global depsgraph_updates_locked
|
|
depsgraph_updates_locked = True
|
|
if bpy.app.timers.is_registered(unlock_depsgraph_updates):
|
|
bpy.app.timers.unregister(unlock_depsgraph_updates)
|
|
bpy.app.timers.register(unlock_depsgraph_updates, first_interval=1)
|
|
|
|
|
|
import time
|
|
|
|
|
|
@persistent
|
|
def on_depsgraph_update(scene, depsgraph):
|
|
global depsgraph_updates_locked
|
|
if not bpy.context.mode.startswith("EDIT") and not depsgraph_updates_locked:
|
|
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
|
|
):
|
|
lock_depsgraph_updates()
|
|
|
|
def later():
|
|
if (
|
|
not "lock_depsgraph_update_ntimes" in scene.abc3d_data
|
|
or scene.abc3d_data["lock_depsgraph_update_ntimes"] <= 0
|
|
):
|
|
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():
|
|
print(f"REGISTER {utils.prefix()}")
|
|
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}")
|
|
|
|
# autostart 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.init()
|
|
|
|
|
|
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)
|
|
|
|
if on_depsgraph_update in bpy.app.handlers.depsgraph_update_post:
|
|
bpy.app.handlers.depsgraph_update_post.remove(on_depsgraph_update)
|
|
|
|
del bpy.types.Scene.abc3d_data
|
|
print(f"UNREGISTER {utils.prefix()}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
register()
|