only import on demand

This commit is contained in:
themancalledjakob 2024-08-21 14:42:53 +02:00
parent e23369df94
commit 25ef83878a
4 changed files with 722 additions and 107 deletions

View file

@ -25,10 +25,12 @@ if "bpy" in locals():
importlib.reload(Font)
importlib.reload(utils)
importlib.reload(butils)
importlib.reload(bimport)
else:
from .common import Font
from .common import utils
from . import butils
from . import bimport
import bpy
import math
@ -173,25 +175,35 @@ class ABC3D_text_properties(bpy.types.PropertyGroup):
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.update_callback(context)
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()
face_name: bpy.props.StringProperty()
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=update_callback
update=glyphs_update_callback
)
letter_spacing: bpy.props.FloatProperty(
update=update_callback,
@ -233,9 +245,9 @@ class ABC3D_text_properties(bpy.types.PropertyGroup):
#TODO: simply, merge, cut cut cut
class ABC3D_data(bpy.types.PropertyGroup):
available_fonts: bpy.props.CollectionProperty(type=ABC3D_available_font, name="name of the collection property")
available_fonts: bpy.props.CollectionProperty(type=ABC3D_available_font, name="Available fonts")
active_font_index: bpy.props.IntProperty()
available_texts: bpy.props.CollectionProperty(type=ABC3D_text_properties, name="")
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
@ -320,8 +332,33 @@ class ABC3D_PT_FontList(bpy.types.Panel):
abc3d = scene.abc3d
abc3d_data = scene.abc3d_data
layout.label(text="Loaded Fonts")
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:")
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 = box.row(); row.scale_y = scale_y
row.label(text=text)
n_rows = int(len(loaded_glyphs) / n)
box.row().label(text=f"Loaded Glyphs:", desription="")
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 = box.row(); row.scale_y = scale_y
row.label(text=text)
class ABC3D_PT_TextPlacement(bpy.types.Panel):
bl_label = "Place Text"
@ -403,13 +440,17 @@ class ABC3D_PT_TextManagement(bpy.types.Panel):
for i in remove_list:
if type(abc3d_data.available_texts[i].text_object) != type(None):
del mom[f"{utils.prefix()}_linked_textobject"]
del mom[f"{utils.prefix()}_font_name"]
del mom[f"{utils.prefix()}_face_name"]
del mom[f"{utils.prefix()}_font_size"]
del mom[f"{utils.prefix()}_letter_spacing"]
del mom[f"{utils.prefix()}_orientation"]
del mom[f"{utils.prefix()}_translation"]
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")
abc3d_data.available_texts.remove(i)
for i, t in enumerate(abc3d_data.available_texts):
@ -422,7 +463,7 @@ class ABC3D_PT_TextManagement(bpy.types.Panel):
if active_text_index != abc3d_data.active_text_index:
abc3d_data.active_text_index = active_text_index
butils.run_in_main_thread(update)
# butils.run_in_main_thread(update)
return True
@ -564,9 +605,18 @@ class ABC3D_OT_LoadInstalledFonts(bpy.types.Operator):
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.label(text="Loading font files can take a long time.")
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)
@ -574,7 +624,10 @@ class ABC3D_OT_LoadInstalledFonts(bpy.types.Operator):
def execute(self, context):
scene = bpy.context.scene
butils.load_installed_fonts()
if self.load_into_memory:
butils.load_installed_fonts()
else:
butils.register_installed_fonts()
butils.ShowMessageBox("Loading Fonts",
'INFO',
"Updating Data Structures.")
@ -744,17 +797,23 @@ class ABC3D_OT_PlaceText(bpy.types.Operator):
while text_id == tt.text_id:
text_id = text_id + 1
t = abc3d_data.available_texts.add()
t.text_id = text_id
t.font_name = self.font_name
t.face_name = self.face_name
# 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_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.translation = self.translation
t.orientation = self.orientation
t.distribution_type = distribution_type
t['text'] = self.text
t['letter_spacing'] = self.letter_spacing
t['font_size'] = self.font_size
t['translation'] = self.translation
t['orientation'] = self.orientation
t['distribution_type'] = distribution_type
butils.prepare_text(t.font_name,
t.face_name,
t.text)
butils.set_text_on_curve(t)
else:
butils.ShowMessageBox(
title="No object selected",
@ -1134,6 +1193,8 @@ class ABC3D_OT_Reporter(bpy.types.Operator):
return {'FINISHED'}
classes = (
bimport.ImportGLTF2,
bimport.GetFontFacesInFile,
ABC3D_addonPreferences,
ABC3D_available_font,
ABC3D_glyph_properties,
@ -1169,7 +1230,7 @@ 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(bpy.ops.abc3d.load_installed_fonts)
def load_handler_unload():
if bpy.app.timers.is_registered(butils.execute_queued_functions):
@ -1180,10 +1241,6 @@ 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)
# for i, t in enumerate(bpy.context.scene.abc3d_data.available_texts):
# # TODO PERFORMANCE: only on demand
# # butils.set_text_on_curve(t)
# pass
def register():
for cls in classes:

431
bimport.py Normal file
View file

@ -0,0 +1,431 @@
import bpy
from bpy.props import (StringProperty,
BoolProperty,
EnumProperty,
IntProperty,
FloatProperty,
CollectionProperty)
from bpy.types import Operator
from bpy_extras.io_utils import ImportHelper, ExportHelper
from io_scene_gltf2 import ConvertGLTF2_Base
from .common import Font
# taken from blender_git/blender/scripts/addons/io_scene_gltf2/__init__.py
def get_font_faces_in_file(filepath):
from io_scene_gltf2.io.imp.gltf2_io_gltf import glTFImporter, ImportError
try:
import_settings = { 'import_user_extensions': [] }
gltf_importer = glTFImporter(filepath, import_settings)
gltf_importer.read()
gltf_importer.checks()
out = []
for node in gltf_importer.data.nodes:
if type(node.extras) != type(None) \
and "glyph" in node.extras \
and not ("type" in node.extras and node.extras["type"] is "metrics"):
out.append(node.extras)
return out
except ImportError as e:
return None
# taken from blender_git/blender/scripts/addons/io_scene_gltf2/__init__.py
class GetFontFacesInFile(Operator, ImportHelper):
"""Load a glTF 2.0 font and check which faces are in there"""
bl_idname = f"abc3d.check_font_gltf"
bl_label = 'Check glTF 2.0 Font'
bl_options = {'REGISTER', 'UNDO'}
files: CollectionProperty(
name="File Path",
type=bpy.types.OperatorFileListElement,
)
# bpy.ops.abc3d.check_font_gltf(filepath="/home/jrkb/.config/blender/4.1/datafiles/abc3d/fonts/JRKB_LOL.glb")
found_fonts = []
def execute(self, context):
return self.check_gltf2(context)
def check_gltf2(self, context):
import os
import sys
if self.files:
# Multiple file check
ret = {'CANCELLED'}
dirname = os.path.dirname(self.filepath)
for file in self.files:
path = os.path.join(dirname, file.name)
if self.unit_check(path) == {'FINISHED'}:
ret = {'FINISHED'}
return ret
else:
# Single file check
return self.unit_check(self.filepath)
def unit_check(self, filename):
self.found_fonts.append(["LOL","WHATEVER"])
return {'FINISHED'}
class ImportGLTF2(Operator, ConvertGLTF2_Base, ImportHelper):
"""Load a glTF 2.0 font"""
bl_idname = f"abc3d.import_font_gltf"
bl_label = 'Import glTF 2.0 Font'
bl_options = {'REGISTER', 'UNDO'}
filter_glob: StringProperty(default="*.glb;*.gltf", options={'HIDDEN'})
files: CollectionProperty(
name="File Path",
type=bpy.types.OperatorFileListElement,
)
loglevel: IntProperty(
name='Log Level',
description="Log Level")
import_pack_images: BoolProperty(
name='Pack Images',
description='Pack all images into .blend file',
default=True
)
merge_vertices: BoolProperty(
name='Merge Vertices',
description=(
'The glTF format requires discontinuous normals, UVs, and '
'other vertex attributes to be stored as separate vertices, '
'as required for rendering on typical graphics hardware. '
'This option attempts to combine co-located vertices where possible. '
'Currently cannot combine verts with different normals'
),
default=False,
)
import_shading: EnumProperty(
name="Shading",
items=(("NORMALS", "Use Normal Data", ""),
("FLAT", "Flat Shading", ""),
("SMOOTH", "Smooth Shading", "")),
description="How normals are computed during import",
default="NORMALS")
bone_heuristic: EnumProperty(
name="Bone Dir",
items=(
("BLENDER", "Blender (best for import/export round trip)",
"Good for re-importing glTFs exported from Blender, "
"and re-exporting glTFs to glTFs after Blender editing. "
"Bone tips are placed on their local +Y axis (in glTF space)"),
("TEMPERANCE", "Temperance (average)",
"Decent all-around strategy. "
"A bone with one child has its tip placed on the local axis "
"closest to its child"),
("FORTUNE", "Fortune (may look better, less accurate)",
"Might look better than Temperance, but also might have errors. "
"A bone with one child has its tip placed at its child's root. "
"Non-uniform scalings may get messed up though, so beware"),
),
description="Heuristic for placing bones. Tries to make bones pretty",
default="BLENDER",
)
guess_original_bind_pose: BoolProperty(
name='Guess Original Bind Pose',
description=(
'Try to guess the original bind pose for skinned meshes from '
'the inverse bind matrices. '
'When off, use default/rest pose as bind pose'
),
default=True,
)
import_webp_texture: BoolProperty(
name='Import WebP textures',
description=(
"If a texture exists in WebP format, "
"loads the WebP texture instead of the fallback PNG/JPEG one"
),
default=False,
)
glyphs: StringProperty(
name='Import only these glyphs',
description=(
"Loading glyphs is expensive, if the meshes are huge"
"So we can filter all glyphs out that we do not want"
),
default="A",
)
marker_property: StringProperty(
name="Mark imported objects with this custom property.",
default="font_import",
)
font_name: StringProperty(
name="If defined, only import this font",
default="",
)
face_name: StringProperty(
name="If defined, only import this font face",
default="",
)
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False # No animation.
layout.prop(self, 'import_pack_images')
layout.prop(self, 'merge_vertices')
layout.prop(self, 'import_shading')
layout.prop(self, 'guess_original_bind_pose')
layout.prop(self, 'bone_heuristic')
layout.prop(self, 'export_import_convert_lighting_mode')
layout.prop(self, 'import_webp_texture')
def invoke(self, context, event):
import sys
preferences = bpy.context.preferences
for addon_name in preferences.addons.keys():
try:
if hasattr(sys.modules[addon_name], 'glTF2ImportUserExtension') or hasattr(sys.modules[addon_name], 'glTF2ImportUserExtensions'):
importer_extension_panel_unregister_functors.append(sys.modules[addon_name].register_panel())
except Exception:
pass
self.has_active_importer_extensions = len(importer_extension_panel_unregister_functors) > 0
return ImportHelper.invoke(self, context, event)
def execute(self, context):
return self.import_gltf2(context)
def import_gltf2(self, context):
import os
self.set_debug_log()
import_settings = self.as_keywords()
user_extensions = []
import sys
preferences = bpy.context.preferences
for addon_name in preferences.addons.keys():
try:
module = sys.modules[addon_name]
except Exception:
continue
if hasattr(module, 'glTF2ImportUserExtension'):
extension_ctor = module.glTF2ImportUserExtension
user_extensions.append(extension_ctor())
import_settings['import_user_extensions'] = user_extensions
if self.files:
# Multiple file import
ret = {'CANCELLED'}
dirname = os.path.dirname(self.filepath)
for file in self.files:
path = os.path.join(dirname, file.name)
if self.unit_import(path, import_settings) == {'FINISHED'}:
ret = {'FINISHED'}
return ret
else:
# Single file import
return self.unit_import(self.filepath, import_settings)
def unit_import(self, filename, import_settings):
import time
from io_scene_gltf2.io.imp.gltf2_io_gltf import glTFImporter, ImportError
from io_scene_gltf2.blender.imp.gltf2_blender_gltf import BlenderGlTF
from io_scene_gltf2.blender.imp.gltf2_blender_vnode import VNode, compute_vnodes
from io_scene_gltf2.blender.com.gltf2_blender_extras import set_extras
from io_scene_gltf2.blender.imp.gltf2_blender_node import BlenderNode
try:
gltf = glTFImporter(filename, import_settings)
gltf.read()
gltf.checks()
# start filtering glyphs like this:
# - collect indices of nodes that contain our glyphs
# - collect indices of their meshes
# - collect the node's parent tree
# - use these indices to create new lists of nodes and meshes
# - update the scene tree to contain only our nodes
# - replace the node and mesh list with ours
# indices of meshes to keep
mesh_indices = []
# indices of nodes to keep
node_indices = []
# convenience function to add a node to the indices
def add_node(node, recursive=True):
node_index = gltf.data.nodes.index(node)
if node_index not in node_indices:
node_indices.append(node_index)
if type(node.mesh) != type(None) and node.mesh >= 0:
mesh_index = node.mesh
if mesh_index not in mesh_indices:
mesh_indices.append(mesh_index)
if recursive and type(node.children) != type(None):
for c in node.children:
child = gltf.data.nodes[c]
add_node(child)
# convenience function to add a mesh to the indices
def add_parent_node(node, recursive=True):
index = gltf.data.nodes.index(node)
for parent in gltf.data.nodes:
if type(parent.children) != type(None) and index in parent.children:
add_node(parent, False)
if recursive:
add_parent_node(parent)
# populate our node_indices and mesh_indices
# by iterating through the nodes and check if they are
# indeed representing a glyph we want
for node in gltf.data.nodes:
# :-O woah
if type(node.extras) != type(None) \
and "glyph" in node.extras \
and (node.extras["glyph"] in self.glyphs \
or len(self.glyphs) == 0) \
and (self.font_name == "" or \
( "font_name" in node.extras \
and (node.extras["font_name"] in self.font_name \
or len(self.glyphs) == 0))) \
and (self.face_name == "" or \
( "face_name" in node.extras \
and (node.extras["face_name"] in self.face_name \
or len(self.glyphs) == 0))):
# if there is a match, add the node incl children ..
add_node(node)
# .. and their parents recursively
add_parent_node(node)
# in the end we need the objects, not the indices
# so let's prepare empy lists
meshes = []
nodes = []
# the indices will be off, as we have fewer elements
# so let's have a lookup table
mesh_index_table = {}
node_index_table = {}
# first, add all meshes and fill in lookup table
for mesh_index, mesh in enumerate(gltf.data.meshes):
if mesh_index in mesh_indices:
meshes.append(mesh)
mesh_index_table[mesh_index] = len(meshes) - 1
# second, add all nodes and fill in lookup table
# nodes also refer to their meshes
for node_index, node in enumerate(gltf.data.nodes):
if node_index in node_indices:
if type(node.mesh) != type(None):
node.mesh = mesh_index_table[node.mesh]
nodes.append(node)
node_index_table[node_index] = len(nodes) - 1
# the indices to children are messed up.
# some children are lost :(
# and some have different indices
for node in nodes:
if type(node.children) != type(None):
children = [] # brand new children
for i, c in enumerate(node.children):
# check if children are lost
if c in node_indices:
children.append(node_index_table[c])
# now replace old children with the new, however
# if we don't have children, we don't even need a list!
node.children = None if len(children) == 0 else children
# last step, kick nodes out of the scene tree if they're lost
for s in gltf.data.scenes:
scene_nodes = []
for n in s.nodes:
if n in node_indices:
scene_nodes.append(node_index_table[n])
s.nodes = scene_nodes
# very last step, replace nodes and meshes
gltf.data.nodes = nodes
gltf.data.meshes = meshes
# that's fucking it, we're done!
# hand over back to default blender behaviour :-)
# or.. not! blender will do some funny scene stuff
# which we don't want.
# so let's do it quick
print("Data are loaded, start creating Blender stuff")
start_time = time.time()
# first, convert gltf to blender
BlenderGlTF.set_convert_functions(gltf)
# compute things
BlenderGlTF.pre_compute(gltf)
compute_vnodes(gltf)
# apparently we need a scene, because
# when creating the objects, it will link the objects here
gltf.blender_scene = bpy.context.scene.name
def create_blender_object(gltf, vi, nodes):
vnode = gltf.vnodes[vi]
if vnode.type == VNode.Object:
if vnode.parent is not None:
if not hasattr(gltf.vnodes[vnode.parent],
"blender_object"):
create_blender_object(gltf,
vnode.parent,
nodes)
if not hasattr(vnode,
"blender_object"):
obj = BlenderNode.create_object(gltf, vi)
obj["font_import"] = True
n_vars = vars(nodes[vi])
if "extras" in n_vars:
set_extras(obj, n_vars["extras"])
if "glyph" in n_vars["extras"] and \
not ("type" in n_vars["extras"] and \
n_vars["extras"]["type"] == "metrics"):
obj["type"] = "glyph"
for vi, vnode in gltf.vnodes.items():
create_blender_object(gltf, vi, nodes)
elapsed_s = "{:.2f}s".format(time.time() - start_time)
print("font import gltf finished in " + elapsed_s)
gltf.log.removeHandler(gltf.log_handler)
return {'FINISHED'}
except ImportError as e:
self.report({'ERROR'}, e.args[0])
return {'CANCELLED'}
def set_debug_log(self):
import logging
if bpy.app.debug_value == 0:
self.loglevel = logging.CRITICAL
elif bpy.app.debug_value == 1:
self.loglevel = logging.ERROR
elif bpy.app.debug_value == 2:
self.loglevel = logging.WARNING
elif bpy.app.debug_value == 3:
self.loglevel = logging.INFO
else:
self.loglevel = logging.NOTSET

215
butils.py
View file

@ -314,7 +314,6 @@ def find_font_face_object(font_obj, face_name):
return None
def move_in_fontcollection(obj, fontcollection, allow_duplicates=False):
# parent nesting structure
# the font object
font_obj = find_font_object(fontcollection,
@ -377,80 +376,104 @@ def move_in_fontcollection(obj, fontcollection, allow_duplicates=False):
return obj
def load_font_from_filepath(filepath):
def register_font_from_filepath(filepath):
from .bimport import get_font_faces_in_file
availables = get_font_faces_in_file(filepath)
fonts = {}
for a in availables:
font_name = a["font_name"]
face_name = a["face_name"]
glyph = a["glyph"]
if not font_name in fonts:
fonts[font_name] = {}
if not face_name in fonts[font_name]:
fonts[font_name][face_name] = []
fonts[font_name][face_name].append(glyph)
for font_name in fonts:
for face_name in fonts[font_name]:
Font.register_font(font_name,
face_name,
fonts[font_name][face_name],
filepath)
def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""):
if not filepath.endswith(".glb") and not filepath.endswith(".gltf"):
ShowMessageBox(f"{bl_info['name']} Font loading error", 'ERROR', f"Filepath({filepath}) is not a *.glb or *.gltf file")
return False
abc3d_data = bpy.context.scene.abc3d_data
allObjectsBefore = []
for ob in bpy.data.objects:
allObjectsBefore.append(ob.name)
bpy.ops.import_scene.gltf(filepath=filepath)
marker_property = "font_import"
bpy.ops.abc3d.import_font_gltf(filepath=filepath,
glyphs=glyphs,
marker_property=marker_property,
font_name=font_name,
face_name=face_name)
fontcollection = bpy.data.collections.get("ABC3D")
if fontcollection is None:
fontcollection = bpy.data.collections.new("ABC3D")
modified_font_faces = []
all_glyph_os = []
remove_list = []
all_objects = []
for o in bpy.data.objects:
all_objects.append(o)
for o in all_objects:
o_exists = True
try:
o, o.name
except ReferenceError as e:
o_exists = False
if o_exists and o.name not in allObjectsBefore:
# must be new
if ("glyph" in o.keys()
and "face_name" in o.keys()
and "font_name" in o.keys()
and not ("type" in o.keys() and o["type"] == "metrics")
and not is_metrics_object(o)
):
glyph_id = o["glyph"]
font_name = o["font_name"]
face_name = o["face_name"]
# ShowMessageBox("Loading Font", "INFO", f"adding glyph {glyph_id} for {font_name} {face_name}")
print(f"adding glyph {glyph_id} for {font_name} {face_name}")
glyph_obj = move_in_fontcollection(
o,
fontcollection)
Font.add_glyph(
font_name,
face_name,
glyph_id,
glyph_obj)
for c in o.children:
if is_metrics_object(c):
add_metrics_obj_from_bound_box(glyph_obj,
bound_box_as_array(c.bound_box))
if glyph_obj != o:
remove_list.append(o)
# found = False
# for f in abc3d_data.available_fonts.values():
# print(f"has in availables {f.font_name} {f.face_name}")
# 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
# print(f"{__name__} added {font_name} {face_name}")
elif o_exists:
for o in bpy.context.scene.objects:
if marker_property in o:
if "type" in o and o["type"] == "glyph":
all_glyph_os.append(o)
else:
remove_list.append(o)
for o in remove_list:
try:
bpy.data.objects.remove(o, do_unlink=True)
except ReferenceError as e:
print(f"{__name__} could not remove object, because it doesn't exist")
print(f"{__name__}: loaded font from {filepath}")
for o in all_glyph_os:
glyph_id = o["glyph"]
font_name = o["font_name"]
face_name = o["face_name"]
del o[marker_property]
glyph_obj = move_in_fontcollection(
o,
fontcollection)
Font.add_glyph(
font_name,
face_name,
glyph_id,
glyph_obj)
for c in o.children:
if is_metrics_object(c):
add_metrics_obj_from_bound_box(glyph_obj,
bound_box_as_array(c.bound_box))
modified_font_faces.append({"font_name": font_name,
"face_name": face_name})
if glyph_obj != o:
remove_list.append(o)
for mff in modified_font_faces:
glyphs = []
face = Font.fonts[mff["font_name"]].faces[mff["face_name"]]
# iterate glyphs
for g in face.glyphs:
# iterate alternates
for glyph in face.glyphs[g]:
glyphs.append(glyph)
if len(glyphs) > 0:
add_default_metrics_to_objects(glyphs)
# calculate unit factor
h = get_glyph_height(glyphs[0])
if h != 0:
face.unit_factor = 1 / h
update_available_fonts()
remove_list = []
for o in bpy.context.scene.collection.all_objects:
if not o.name in fontcollection.all_objects:
if marker_property in o and o[marker_property] == True:
remove_list.append(o)
simply_delete_objects(remove_list)
# completely_delete_objects(remove_list)
def update_available_fonts():
abc3d_data = bpy.context.scene.abc3d_data
@ -491,11 +514,24 @@ def load_installed_fonts():
if file.endswith(".glb") or file.endswith(".gltf"):
font_path = os.path.join(font_dir, file)
# ShowMessageBox("Loading Font", "INFO", f"loading font from {font_path}")
print(f"loading font from {font_path}")
for f in bpy.context.scene.abc3d_data.available_fonts.values():
print(f"available font: {f.font_name} {f.face_name}")
# print(f"loading font from {font_path}")
# for f in bpy.context.scene.abc3d_data.available_fonts.values():
# print(f"available font: {f.font_name} {f.face_name}")
register_font_from_filepath(font_path)
load_font_from_filepath(font_path)
def register_installed_fonts():
preferences = getPreferences(bpy.context)
font_dir = f"{preferences.assets_dir}/fonts"
for file in os.listdir(font_dir):
if file.endswith(".glb") or file.endswith(".gltf"):
font_path = os.path.join(font_dir, file)
# ShowMessageBox("Loading Font", "INFO", f"loading font from {font_path}")
# print(f"loading font from {font_path}")
# for f in bpy.context.scene.abc3d_data.available_fonts.values():
# print(f"available font: {f.font_name} {f.face_name}")
register_font_from_filepath(font_path)
def ShowMessageBox(title = "Message Box", icon = 'INFO', message=""):
"""Show a simple message box
@ -531,12 +567,15 @@ def ShowMessageBox(title = "Message Box", icon = 'INFO', message=""):
self.layout.label(text=n)
bpy.context.window_manager.popup_menu(draw, title = title, icon = icon)
def completely_delete_objects(objs):
def simply_delete_objects(objs):
context_override = bpy.context.copy()
context_override["selected_objects"] = list(objs)
with bpy.context.temp_override(**context_override):
bpy.ops.object.delete()
def completely_delete_objects(objs):
simply_delete_objects(objs)
# remove deleted objects
# this is necessary
for g in objs:
@ -578,7 +617,23 @@ def get_glyph_advance(glyph_obj):
return abs(c.bound_box[4][0] - c.bound_box[0][0])
return abs(glyph_obj.bound_box[4][0] - glyph_obj.bound_box[0][0])
def set_text_on_curve(text_properties):
def get_glyph_height(glyph_obj):
for c in glyph_obj.children:
if is_metrics_object(c):
return abs(c.bound_box[0][1] - c.bound_box[3][1])
return abs(glyph_obj.bound_box[0][1] - glyph_obj.bound_box[3][1])
def prepare_text(font_name, face_name, text):
loaded, missing, loadable, files = Font.test_glyphs_availability(
font_name,
face_name,
text)
if len(loadable) > 0:
for filepath in files:
load_font_from_filepath(filepath, loadable, font_name, face_name)
return True
def set_text_on_curve(text_properties, recursive=True):
# starttime = time.perf_counter_ns()
mom = text_properties.text_object
if mom.type != "CURVE":
@ -605,6 +660,22 @@ def set_text_on_curve(text_properties):
# if we regenerate.... delete objects
if regenerate:
# loaded, missing, maybe, files = Font.test_glyphs_availability(
# text_properties.font_name,
# text_properties.face_name,
# text_properties.text)
# if len(maybe) > 0 and recursive:
# print(f"doing the thing {len(files)} times")
# for filepath in files:
# def loader():
# set_text_on_curve(text_properties, False)
# print(f"loading font from filepath {filepath} {maybe}")
# load_font_from_filepath(filepath, maybe)
# print(f"font: {text_properties.font_name} face: {text_properties.face_name}")
# print("text",text_properties.text)
# text_properties.font_size = text_properties.font_size
# # run_in_main_thread(loader)
# return
completely_delete_objects(glyph_objects)
# context_override = bpy.context.copy()
# context_override["selected_objects"] = list(glyph_objects)
@ -696,7 +767,8 @@ def set_text_on_curve(text_properties):
ob.rotation_quaternion = q
# ob.rotation_quaternion = (mom.matrix_world @ q.to_matrix().to_4x4()).to_quaternion()
scalor = 0.001 * text_properties.font_size
face = Font.fonts[text_properties.font_name].faces[text_properties.face_name]
scalor = face.unit_factor * text_properties.font_size
glyph_advance = get_glyph_advance(glyph) * scalor + text_properties.letter_spacing
@ -929,6 +1001,13 @@ def get_metrics_bound_box(bb, bb_uebermetrics):
metrics[7][0] = bb[7][0]
return metrics
def get_metrics_object(o):
if is_glyph(o):
for c in o.children:
if is_metrics_object(c):
return c
return None
def add_default_metrics_to_objects(objects=None, overwrite_existing=False):
if type(objects) == type(None):
objects=bpy.context.selected_objects

View file

@ -91,10 +91,24 @@ class FontFace:
:param glyphs: dictionary of glyphs, defaults to ``{}``
:type glyphs: dict, optional
:param loaded_glyphs: glyphs currently loaded
:type loaded_glyphs: List[str], optional
:param missing_glyphs: glyphs not present in the fontfile
:type missing_glyphs: List[str], optional
:param filenames: from which file is this face
:type filenames: List[str]
"""
def __init__(self, glyphs = {}):
def __init__(self,
glyphs = {}):
self.glyphs = glyphs
# lists have to be initialized in __init__
# to be attributes per instance.
# otherwise they are static class attributes
self.loaded_glyphs = []
self.missing_glyphs = []
self.glyphs_in_fontfile = []
self.filepaths = []
self.unit_factor = 1.0
class Font:
"""Font holds the faces and various metadata for a font
@ -108,6 +122,19 @@ class Font:
# TODO: better class structure?
# TODO: get fonts and faces directly
def register_font(font_name, face_name, glyphs_in_fontfile, filepath):
if not fonts.keys().__contains__(font_name):
fonts[font_name] = Font({})
if fonts[font_name].faces.get(face_name) == None:
fonts[font_name].faces[face_name] = FontFace({})
fonts[font_name].faces[face_name].glyphs_in_fontfile = glyphs_in_fontfile
else:
fonts[font_name].faces[face_name].glyphs_in_fontfile = \
list(set(fonts[font_name].faces[face_name].glyphs_in_fontfile + glyphs_in_fontfile))
if filepath not in fonts[font_name].faces[face_name].filepaths:
fonts[font_name].faces[face_name].filepaths.append(filepath)
def add_glyph(font_name, face_name, glyph_id, glyph_object):
""" add_glyph adds a glyph to a FontFace
@ -125,15 +152,14 @@ def add_glyph(font_name, face_name, glyph_id, glyph_object):
if not fonts.keys().__contains__(font_name):
fonts[font_name] = Font({})
# print("is it has been added", fonts.keys())
if fonts[font_name].faces.get(face_name) == None:
fonts[font_name].faces[face_name] = FontFace({})
# print("is it has been added faces", fonts[font_name].faces[face_name])
if fonts[font_name].faces[face_name].glyphs.get(glyph_id) == None:
fonts[font_name].faces[face_name].glyphs[glyph_id] = []
# print("is it has been added glyph", fonts[font_name].faces[face_name].glyphs[glyph_id])
fonts[font_name].faces[face_name].glyphs.get(glyph_id).append(glyph_object)
if glyph_id not in fonts[font_name].faces[face_name].loaded_glyphs:
fonts[font_name].faces[face_name].loaded_glyphs.append(glyph_id)
def get_glyph(font_name, face_name, glyph_id, alternate=0):
""" add_glyph adds a glyph to a FontFace
@ -149,7 +175,7 @@ def get_glyph(font_name, face_name, glyph_id, alternate=0):
:return: returns the glyph object, or ``None`` if it does not exist
:rtype: `Object`
"""
# print(fonts)
if not fonts.keys().__contains__(font_name):
print(f"ABC3D::get_glyph: font name({font_name}) not found")
print(fonts.keys())
@ -164,10 +190,32 @@ def get_glyph(font_name, face_name, glyph_id, alternate=0):
glyphs_for_id = face.glyphs.get(glyph_id)
if glyphs_for_id == None or len(glyphs_for_id) <= alternate:
print(f"ABC3D::get_glyph: font({font_name}) face({face_name}) glyph({glyph_id})[{alternate}] not found")
if glyph_id not in fonts[font_name].faces[face_name].missing_glyphs:
fonts[font_name].faces[face_name].missing_glyphs.append(glyph_id)
return None
return fonts[font_name].faces[face_name].glyphs.get(glyph_id)[alternate]
def test_glyphs_availability(font_name, face_name, text):
# maybe there is NOTHING yet
if not fonts.keys().__contains__(font_name) or \
fonts[font_name].faces.get(face_name) == None:
return "", "", text # <loaded>, <missing>, <maybe>
loaded = []
missing = []
maybe = []
for c in text:
if c in fonts[font_name].faces[face_name].loaded_glyphs:
loaded.append(c)
elif c in fonts[font_name].faces[face_name].glyphs_in_fontfile:
maybe.append(c)
else:
if c not in fonts[font_name].faces[face_name].missing_glyphs:
fonts[font_name].faces[face_name].missing_glyphs.append(c)
missing.append(c)
return ''.join(loaded), ''.join(missing), ''.join(maybe), fonts[font_name].faces[face_name].filepaths
def get_loaded_fonts():
return fonts.keys()