diff --git a/README.md b/README.md index 50b7459..a89154c 100644 --- a/README.md +++ b/README.md @@ -16,3 +16,6 @@ ln -s $HOME/git/pointer/neomatter/font3d/font3d_blender_addon $HOME/git/tools/bl bpy.utils.script_paths() ``` then check it for the `addons` directory + +# reload addon in blender: +F3 -> "reload scripts" diff --git a/__init__.py b/__init__.py index a0f430a..3226671 100644 --- a/__init__.py +++ b/__init__.py @@ -14,6 +14,22 @@ bl_info = { "category": "Typography", } +# make sure that modules are reloadable +# when registering +# handy for development +# first import dependencies for the method +import importlib + +# then import dependencies for our addon +if "bpy" in locals(): + importlib.reload(Font) + importlib.reload(utils) + importlib.reload(butils) +else: + from .common import Font + from .common import utils + from . import butils + import bpy import math import io @@ -25,25 +41,18 @@ from random import uniform import time import datetime -def get_timestamp(): - return datetime.datetime.fromtimestamp(time.time()).strftime('%Y.%m.%d-%H:%M:%S') +import re + class SharedVariables(): - fonts: dict = {} + fonts = Font.fonts + def __init__(self, **kv): self.__dict__.update(kv) + shared = SharedVariables() -def mapRange(in_value, in_min, in_max, out_min, out_max, clamp=False): - output = out_min + ((out_max - out_min) / (in_max - in_min)) * (in_value - in_min) - if clamp: - if out_min < out_max: - return min(out_max, max(out_min, output)) - else: - return max(out_max, min(out_min, output)) - else: - return output class FONT3D_OT_Font3D(bpy.types.Operator): """Font 3D""" @@ -66,10 +75,36 @@ class FONT3D_OT_Font3D(bpy.types.Operator): class FONT3D_settings(bpy.types.PropertyGroup): font_path: bpy.props.StringProperty(name="Font path", - description="Where is the font", - default="", - maxlen=1024, - subtype="FILE_PATH") + description="Where is the font", + default="", + maxlen=1024, + subtype="FILE_PATH") + import_infix: bpy.props.StringProperty(name="Font name import infix", + description="The infix which all font objects to import have", + default="_NM_", + maxlen=1024, + ) + + +class FONT3D_available_font(bpy.types.PropertyGroup): + font_name: bpy.props.StringProperty(name="whatever") + +#TODO: simply, merge, cut cut cut +class FONT3D_data(bpy.types.PropertyGroup): + available_fonts: bpy.props.CollectionProperty(type=FONT3D_available_font, name="name of the collection poporotery") + active_font_index: bpy.props.IntProperty() + +class FONT3D_UL_fonts(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + split = layout.split(factor=0.3) + split.label(text="Index: %d" % (index)) + # custom_icon = "OUTLINER_OB_%s" % item.obj_type + # split.prop(item, "name", text="", emboss=False, translate=False) + split.label(text=f"{item.font_name}") # avoids renaming the item by accident + + def invoke(self, context, event): + pass + class FONT3D_PT_panel(bpy.types.Panel): bl_label = "Panel for Font3D" @@ -78,16 +113,26 @@ class FONT3D_PT_panel(bpy.types.Panel): bl_region_type = "UI" def draw(self, context): + global shared layout = self.layout wm = context.window_manager scene = context.scene font3d = scene.font3d + font3d_data = scene.font3d_data + layout.label(text="Load FontFile:") layout.row().prop(font3d, "font_path") layout.row().operator('font3d.loadfont', text='Load Font') + layout.label(text="Available Fonts") + layout.template_list("FONT3D_UL_fonts", "", font3d_data, "available_fonts", font3d_data, "active_font_index") layout.label(text='DEBUG') layout.row().operator('font3d.testfont', text='Test Font') + layout.row().prop(font3d, "import_infix") + layout.row().operator('font3d.create_font_from_objects', text='Create Font') + layout.row().operator('font3d.toggle_font3d_collection', text='Toggle Collection') + layout.label(text='DEBUG END') + class FONT3D_OT_LoadFont(bpy.types.Operator): """Load Font 3D""" @@ -99,7 +144,7 @@ class FONT3D_OT_LoadFont(bpy.types.Operator): global shared scene = bpy.context.scene - print(f"loading da font at path {scene.font3d.font_path}") + # print(f"loading da font at path {scene.font3d.font_path}") currentObjects = [] for ob in bpy.data.objects: @@ -119,10 +164,8 @@ class FONT3D_OT_LoadFont(bpy.types.Operator): for o in bpy.data.objects: if o.name not in currentObjects: if (o.parent == None): - print(f"found root node --> {o.name}") font['name'] = o.name elif o.parent.name.startswith("glyphs"): - print(f"loading glyph --> {o.name}") font['glyphs'].append(o) newObjects.append(o.name) scene.collection.objects.unlink(o) @@ -132,7 +175,8 @@ class FONT3D_OT_LoadFont(bpy.types.Operator): shared.fonts except: shared.fonts = {} - shared.fonts[font['name']] = font + shared.fonts[font['name']] = Font.Font() + shared.fonts[font['name']]['faces']['regular'] = font return {'FINISHED'} @@ -146,22 +190,127 @@ class FONT3D_OT_TestFont(bpy.types.Operator): global shared scene = bpy.context.scene - print(shared.fonts) + + selected = bpy.context.view_layer.objects.active + + glyph = Font.get_glyph("NM_Origin", "Tender", "A").data + ob = bpy.data.objects.new('Duplicate_Linked', glyph) + ob.location = selected.location + ob.scale = (0.01, 0.01, 0.01) + selected.users_collection[0].objects.link(ob) + + return {'FINISHED'} + +class FONT3D_OT_ToggleFont3DCollection(bpy.types.Operator): + """Toggle Font3D Collection""" + bl_idname = "font3d.toggle_font3d_collection" + bl_label = "Toggle Collection visibility" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + scene = context.scene + fontcollection = bpy.data.collections.get("Font3D") + + if fontcollection is None: + self.report({'INFO'}, f"{bl_info['name']}: There is no collection") + elif scene.collection.children.find(fontcollection.name) < 0: + scene.collection.children.link(fontcollection) + self.report({'INFO'}, f"{bl_info['name']}: show collection") + else: + scene.collection.children.unlink(fontcollection) + self.report({'INFO'}, f"{bl_info['name']}: hide collection") + + return {'FINISHED'} + + +class FONT3D_OT_CreateFontFromObjects(bpy.types.Operator): + """Create Font from open objects""" + bl_idname = "font3d.create_font_from_objects" + bl_label = "Create Font" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + global shared + scene = bpy.context.scene + font3d = scene.font3d + font3d_data = scene.font3d_data + + fontcollection = bpy.data.collections.get("Font3D") + + if fontcollection is None: + fontcollection = bpy.data.collections.new("Font3D") + + ifxsplit = font3d.import_infix.split('_') + font_name = f"{ifxsplit[1]}_{ifxsplit[2]}" + face_name = ifxsplit[3] + + added_font = False + + font3d_data.available_fonts.clear() + + currentObjects = [] + for o in bpy.data.objects: + if o.name not in currentObjects: + if font3d.import_infix in o.name: + uc = o.users_collection + regex = f"{font3d.import_infix}(.)*" + name = re.sub(regex, "", o.name) + glyph_id = "unknown" + if len(name) == 1: + glyph_id = name + elif name in Font.name_to_glyph_d: + glyph_id = Font.name_to_glyph_d[name] + + if glyph_id != "unknown": + bpy.data.objects[o.name]["glyph"] = glyph_id + butils.move_in_fontcollection( + o, + fontcollection, + scene) + Font.add_glyph( + font_name, + face_name, + glyph_id, + o) + added_font = True + + #TODO: is there a better way to iterate over a CollectionProperty? + found = False + for f in font3d_data.available_fonts.values(): + if f.font_name == font_name: + found = True + break + if not found: + f = font3d_data.available_fonts.add() + f.font_name = font_name + + else: + self.report({'INFO'}, f"did not understand glyph {name}") return {'FINISHED'} classes = ( FONT3D_OT_Font3D, + FONT3D_available_font, + FONT3D_data, FONT3D_settings, + FONT3D_UL_fonts, FONT3D_PT_panel, FONT3D_OT_TestFont, - FONT3D_OT_LoadFont + FONT3D_OT_LoadFont, + FONT3D_OT_ToggleFont3DCollection, + FONT3D_OT_CreateFontFromObjects, ) def register(): for cls in classes: bpy.utils.register_class(cls) bpy.types.Scene.font3d = bpy.props.PointerProperty(type=FONT3D_settings) + bpy.types.Scene.font3d_data = bpy.props.PointerProperty(type=FONT3D_data) + print(f"REGISTER {bl_info['name']}") + + print(utils.get_timestamp()) + print(shared.fonts) # would love to properly auto start this, but IT DOES NOT WORK # if load_handler not in bpy.app.handlers.load_post: # bpy.app.handlers.load_post.append(load_handler) @@ -174,6 +323,8 @@ def unregister(): bpy.utils.unregister_class(cls) del bpy.types.Scene.font3d + del bpy.types.Scene.font3d_data + print(f"UNREGISTER {bl_info['name']}") if __name__ == '__main__': register() diff --git a/_vimrc_local.vim b/_vimrc_local.vim index b4166ea..51e03a8 100644 --- a/_vimrc_local.vim +++ b/_vimrc_local.vim @@ -6,4 +6,12 @@ let g:jedi#environment_path = "venv" """"""""""""""""""""""""""""""""" ALE +"let g:ale_python_pylint_executable = '/home/jrkb/git/pointer/neomatter/font3d/font3d_blender_addon/venv/bin/pylint' +"let g:ale_python_executable='/home/jrkb/git/pointer/neomatter/font3d/font3d_blender_addon/venv/bin/python' +"let g:ale_python_pylint_use_global=1 +"let g:ale_use_global_executables=1 +"let g:ale_python_auto_pipenv=1 +"let g:ale_python_auto_virtualenv=1 +"let g:ale_virtualenv_dir_names = ['venv'] + let g:ale_pattern_options = {'\.py$': {'ale_enabled': 0}} diff --git a/butils.py b/butils.py new file mode 100644 index 0000000..18130dc --- /dev/null +++ b/butils.py @@ -0,0 +1,32 @@ +import bpy + + +def get_parent_collection_names(collection, parent_names): + for parent_collection in bpy.data.collections: + if collection.name in parent_collection.children.keys(): + parent_names.append(parent_collection.name) + get_parent_collection_names(parent_collection, parent_names) + return + + +def turn_collection_hierarchy_into_path(obj): + parent_collection = obj.users_collection[0] + parent_names = [] + parent_names.append(parent_collection.name) + get_parent_collection_names(parent_collection, parent_names) + parent_names.reverse() + return '\\'.join(parent_names) + +def move_in_fontcollection(obj, fontcollection, scene): + # print(turn_collection_hierarchy_into_path(obj)) + if scene.collection.objects.find(obj.name) >= 0: + scene.collection.objects.unlink(obj) + if fontcollection.objects.find(obj.name) < 0: + fontcollection.objects.link(obj) + if fontcollection.objects.find("glyphs") < 0: + empty = bpy.data.objects.new("glyphs", None) + empty.empty_display_type = 'PLAIN_AXES' + fontcollection.objects.link(empty) + glyphs = fontcollection.objects.get("glyphs") + if obj.parent != glyphs: + obj.parent = glyphs diff --git a/common/Font.py b/common/Font.py new file mode 100644 index 0000000..e99079c --- /dev/null +++ b/common/Font.py @@ -0,0 +1,118 @@ +from typing import TypedDict +from typing import Dict +from dataclasses import dataclass + +# convenience dictionary for translating names to glyph ids +name_to_glyph_d = { + "zero": "0", + "one": "1", + "two": "2", + "three": "3", + "four": "4", + "five": "5", + "six": "6", + "seven": "7", + "eight": "8", + "nine": "9", + "ampersand": "&", + "backslash": "\\", + "colon": ":", + "comma": ",", + "equal": "=", + "exclam": "!", + "hyphen": "-", + "minus": "−", + "parenleft": "(", + "parenright": "(", + "period": ".", + "plus": "+", + "question": "?", + "quotedblleft": "“", + "quotedblright": "”", + "semicolon": ";", + "slash": "/", + } + + +class FontFace: + """FontFace is a class holding glyphs + + :param glyphs: dictionary of glyphs, defaults to ``{}`` + :type glyphs: dict, optional + """ + def __init__(self, glyphs = {}): + self.glyphs = glyphs + + +class Font: + """Font holds the faces and various metadata for a font + + :param faces: dictionary of faces, defaults to ``Dict[str, FontFace]`` + :type faces: Dict[str, FontFace] + """ + def __init__(self, faces = Dict[str, FontFace]): + self.faces = faces + + +# TODO: better class structure? +# TODO: get fonts and faces directly + +def add_glyph(font_name, face_name, glyph_id, glyph_object): + """ add_glyph adds a glyph to a FontFace + it creates the :class:`Font` and :class:`FontFace` if it does not exist yet + + :param font_name: The Font you want to add the glyph to + :type font_name: str + :param face_name: The FontFace you want to add the glyph to + :type face_name: str + :param glyph_id: The glyph_id you want this glyph to be stored under + :type glyph_id: str + :param glyph_object: The object containing the glyph + :type glyph_object: `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) + + +def get_glyph(font_name, face_name, glyph_id): + """ add_glyph adds a glyph to a FontFace + it creates the :class:`Font` and :class:`FontFace` if it does not exist yet + + :param font_name: The :class:`Font` you want to get the glyph from + :type font_name: str + :param face_name: The :class:`FontFace` you want to get the glyph from + :type face_name: str + :param glyph_id: The ``glyph_id`` from the glyph you want + :type glyph_id: str + ... + :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"FONT3D::get_glyph: font name({font_name}) not found") + print(fonts.keys()) + return None + + if fonts[font_name].faces.get(face_name) == None: + print(f"FONT3D::get_glyph: font face({face_name}) not found") + print(fonts[font_name].faces.keys()) + return None + + if fonts[font_name].faces[face_name].glyphs.get(glyph_id) == None: + print(f"FONT3D::get_glyph: glyph id({glyph_id}) not found") + return None + + return fonts[font_name].faces[face_name].glyphs.get(glyph_id)[0] + +# holds all fonts +fonts = {} diff --git a/common/utils.py b/common/utils.py new file mode 100644 index 0000000..7843db9 --- /dev/null +++ b/common/utils.py @@ -0,0 +1,18 @@ + +import time +import datetime + +def get_timestamp(): + return datetime.datetime \ + .fromtimestamp(time.time()) \ + .strftime('%Y.%m.%d-%H:%M:%S') + +def mapRange(in_value, in_min, in_max, out_min, out_max, clamp=False): + output = out_min + ((out_max - out_min) / (in_max - in_min)) * (in_value - in_min) + if clamp: + if out_min < out_max: + return min(out_max, max(out_min, output)) + else: + return max(out_max, min(out_min, output)) + else: + return output