Compare commits

..

No commits in common. "648d4a6dee0b938be8859a1aed65784d534110ca" and "77f30d51d1125383baa3fc1d66e3122173c22362" have entirely different histories.

5 changed files with 163 additions and 174 deletions

View file

@ -15,13 +15,12 @@ import importlib
bl_info = {
"name": "ABC3D",
"author": "Jakob Schlötter, Studio Pointer*",
"version": (0, 0, 3),
"version": (0, 0, 2),
"blender": (4, 1, 0),
"location": "VIEW3D",
"description": "Convenience addon for 3D fonts",
"category": "Typography",
}
# NOTE: also change version in common/utils.py
# make sure that modules are reloadable
# when registering
@ -312,6 +311,7 @@ class ABC3D_UL_texts(bpy.types.UIList):
def invoke(self, context, event):
pass
class ABC3D_PT_Panel(bpy.types.Panel):
bl_label = f"{__name__} panel"
bl_category = "ABC3D"
@ -333,6 +333,27 @@ class ABC3D_PT_Panel(bpy.types.Panel):
text="open asset directory", icon='FILEBROWSER')
# class ABC3D_PT_LoadFontPanel(bpy.types.Panel):
# bl_label = "Install a new font"
# bl_parent_id = "ABC3D_PT_Panel"
# bl_category = "ABC3D"
# bl_space_type = "VIEW_3D"
# bl_region_type = "UI"
# def draw(self, context):
# layout = self.layout
# wm = context.window_manager
# scene = context.scene
# abc3d_data = scene.abc3d_data
# box = layout.box()
# box.row().label(text="1. Select fontfile")
# box.row().prop(context.scene.abc3d_data, "font_path")
# box.row().label(text="2. Install it:")
# box.row().operator(f"{__name__}.install_font", text='Install new font')
class ABC3D_PT_FontList(bpy.types.Panel):
bl_label = "Font List"
bl_parent_id = "ABC3D_PT_Panel"
@ -384,10 +405,10 @@ class ABC3D_PT_FontList(bpy.types.Panel):
row.scale_y = scale_y
row.label(text=text)
row = layout.row()
oper_lf = row.operator(f"{__name__}.load_font",
oper = row.operator(f"{__name__}.load_font",
text='Load all glyphs in memory')
oper_lf.font_name = font_name
oper_lf.face_name = face_name
oper.font_name = font_name
oper.face_name = face_name
class ABC3D_PT_TextPlacement(bpy.types.Panel):
@ -915,7 +936,6 @@ class ABC3D_OT_RemoveText(bpy.types.Operator):
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")
@ -1048,7 +1068,7 @@ class ABC3D_OT_PlaceText(bpy.types.Operator):
# t.text)
# or this:
# butils.set_text_on_curve(t)
else:
# else:
butils.ShowMessageBox(
title="No object selected",
message=(
@ -1374,7 +1394,7 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator):
font_name,
face_name,
glyph_id,
bpy.types.PointerProperty(o))
o)
# TODO: is there a better way to iterate over a CollectionProperty?
found = False
@ -1397,6 +1417,65 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator):
return {'FINISHED'}
class ABC3D_PT_RightPropertiesPanel(bpy.types.Panel):
"""Creates a Panel in the Object properties window"""
bl_label = f"{bl_info['name']}"
bl_idname = "ABC3D_PT_RightPropertiesPanel"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "object"
@classmethod
def poll(self, context):
# only show the panel, if it's a textobject or a glyph
is_text = type(next((t for t in context.scene.abc3d_data.available_texts if t.text_object ==
context.active_object), None)) != type(None)
is_glyph = type(next((t for t in context.scene.abc3d_data.available_texts if t.text_object ==
context.active_object.parent), None)) != type(None)
return is_text or is_glyph
def draw(self, context):
layout = self.layout
scene = context.scene
abc3d_data = scene.abc3d_data
obj = context.active_object
def is_it_text():
return type(next((t for t in context.scene.abc3d_data.available_texts if t.text_object == context.active_object), None)) != type(None)
def is_it_glyph():
return type(next((t for t in context.scene.abc3d_data.available_texts if t.text_object == context.active_object.parent), None)) != type(None)
is_text = is_it_text()
is_glyph = is_it_glyph()
textobject = obj if is_text else obj.parent if is_glyph else obj
available_text = abc3d_data.available_texts[abc3d_data.active_text_index]
# row = layout.row()
# row.label(text="Hello world!", icon='WORLD_DATA')
# row = layout.row()
# row.label(text="Active object is: " + obj.name)
# row = layout.row()
# row.label(text="text object is: " + textobject.name)
row = layout.row()
row.label(text=f"active text index is: {abc3d_data.active_text_index}")
layout.row().label(text="Text Properties:")
layout.row().prop(available_text, "text")
layout.row().prop(available_text, "letter_spacing")
layout.row().prop(available_text, "font_size")
layout.row().prop(available_text, "offset")
layout.row().prop(available_text, "compensate_curvature")
layout.row().prop(available_text, "ignore_orientation")
layout.column().prop(available_text, "translation")
layout.column().prop(available_text, "orientation")
if is_glyph:
layout.row().label(text="Glyph Properties:")
class ABC3D_OT_Reporter(bpy.types.Operator):
bl_idname = f"{__name__}.reporter"
bl_label = "Report"
@ -1429,6 +1508,7 @@ classes = (
ABC3D_UL_fonts,
ABC3D_UL_texts,
ABC3D_PT_Panel,
# ABC3D_PT_LoadFontPanel,
ABC3D_PT_FontList,
ABC3D_PT_TextPlacement,
ABC3D_PT_TextManagement,
@ -1448,6 +1528,7 @@ classes = (
ABC3D_OT_ToggleABC3DCollection,
ABC3D_OT_SaveFontToFile,
ABC3D_OT_CreateFontFromObjects,
ABC3D_PT_RightPropertiesPanel,
ABC3D_OT_Reporter,
)
@ -1486,6 +1567,7 @@ def detect_text():
def load_used_glyphs():
print("LOAD USED GLYPHS")
scene = bpy.context.scene
abc3d_data = scene.abc3d_data
for t in abc3d_data.available_texts:
@ -1529,33 +1611,19 @@ def on_frame_changed(self, dummy):
# 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:
if not bpy.context.mode.startswith("EDIT"):
for u in depsgraph.updates:
if f"{utils.prefix()}_linked_textobject" in u.id.keys() \
and f"{utils.prefix()}_type" in u.id.keys() \
and u.id[f"{utils.prefix()}_type"] == 'textobject':
linked_textobject = u.id[f"{utils.prefix()}_linked_textobject"]
if u.is_updated_geometry and len(scene.abc3d_data.available_texts) > linked_textobject:
lock_depsgraph_updates()
def later():
if not "lock_depsgraph_update_ntimes" in scene.abc3d_data \
or scene.abc3d_data["lock_depsgraph_update_ntimes"] <= 0:
or scene.abc3d_data["lock_depsgraph_update_ntimes"] == 0:
print("******* not yet")
butils.set_text_on_curve(
scene.abc3d_data.available_texts[linked_textobject])
elif scene.abc3d_data["lock_depsgraph_update_ntimes"] > 0:
@ -1611,9 +1679,6 @@ def unregister():
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 {bl_info['name']}")

View file

@ -1782,6 +1782,7 @@ class ForgejoEngine:
"zipball_url": self.get_zip_url(tag["commit"]["sha"], updater)
} for tag in response]
# -----------------------------------------------------------------------------
# The module-shared class instance,
# should be what's imported to other files

208
butils.py
View file

@ -1,11 +1,10 @@
import importlib
import os
import queue
import re
from multiprocessing import Process
import bpy
import mathutils
import queue
import importlib
import os
import re
from multiprocessing import Process
# import time # for debugging performance
@ -112,8 +111,7 @@ def calc_tangent_on_bezier(bezier_point_1, bezier_point_2, t):
(-3 * (t**2) + 6 * t * (1 - t)) * h2 + (3 * t**2) * p2
).normalized()
from math import acos, pi, radians, sqrt
from math import radians, sqrt, pi, acos
def align_rotations_auto_pivot(mask, input_rotations, vectors, factors, local_main_axis):
output_rotations = [mathutils.Matrix().to_3x3() for _ in range(len(input_rotations))]
@ -451,7 +449,6 @@ def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""):
glyph_obj = move_in_fontcollection(
o,
fontcollection)
glyph_obj_pointer = bpy.types.PointerProperty(glyph_obj)
if glyph_obj == o:
del o[marker_property]
@ -460,7 +457,7 @@ def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""):
font_name,
face_name,
glyph_id,
glyph_obj_pointer)
glyph_obj)
for c in o.children:
if is_metrics_object(c):
add_metrics_obj_from_bound_box(glyph_obj,
@ -475,7 +472,7 @@ def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""):
for g in face.glyphs:
# iterate alternates
for glyph in face.glyphs[g]:
glyphs.append(get_original(glyph))
glyphs.append(glyph)
if len(glyphs) > 0:
add_default_metrics_to_objects(glyphs)
# calculate unit factor
@ -589,19 +586,18 @@ def ShowMessageBox(title = "Message Box", icon = 'INFO', message=""):
bpy.context.window_manager.popup_menu(draw, title = title, icon = icon)
def simply_delete_objects(objs):
completely_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, recursive=True):
def completely_delete_objects(objs):
simply_delete_objects(objs)
# remove deleted objects
# this is necessary
for g in objs:
if type(g) != type(None):
if recursive:
try:
if hasattr(g, "children") and len(g.children) > 0:
completely_delete_objects(g.children)
except ReferenceError as e:
# not important
pass
try:
bpy.data.objects.remove(g, do_unlink=True)
except ReferenceError as e:
@ -676,80 +672,51 @@ def prepare_text(font_name, face_name, text):
return True
def is_bezier(curve):
if curve.type != 'CURVE':
return False
if len(curve.data.splines) < 1:
return False
for spline in curve.data.splines:
if spline.type != 'BEZIER':
return False
return True
def will_regenerate(text_properties):
mom = text_properties.text_object
if len(text_properties.text) != len(text_properties.glyphs):
return True
for i, g in enumerate(text_properties.glyphs):
if not hasattr(g.glyph_object, "type"):
return True
elif g.glyph_object.type != 'EMPTY':
return True
# check if perhaps one glyph was deleted
elif type(g.glyph_object) == type(None):
return True
elif type(g.glyph_object.parent) == type(None):
return True
elif g.glyph_object.parent.users_collection != g.glyph_object.users_collection:
return True
elif len(text_properties.text) > i and g.glyph_id != text_properties.text[i]:
return True
elif len(text_properties.text) > i and (g.glyph_object[f"{utils.prefix()}_font_name"] != text_properties.font_name
or g.glyph_object[f"{utils.prefix()}_face_name"] != text_properties.face_name):
return True
return False
def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4):
"""set_text_on_curve
An earlier reset cancels the other.
To disable reset, set both to false.
:param text_properties: all information necessary to set text on a curve
:type text_properties: ABC3D_text_properties
:param reset_timeout_s: reset external parameters after timeout. (<= 0) = immediate, (> 0) = non-blocking reset timeout in seconds, (False) = no timeout reset
:type reset_timeout_s: float
:param reset_depsgraph_n: reset external parameters after n-th depsgraph update. (<= 0) = immediate, (> 0) = reset after n-th depsgraph update, (False) = no depsgraph reset
:type reset_depsgraph_n: int
"""
return curve.data.splines[0].type == 'BEZIER'
def set_text_on_curve(text_properties, recursive=True):
# starttime = time.perf_counter_ns()
mom = text_properties.text_object
if mom.type != "CURVE":
return False
distribution_type = 'CALCULATE' if is_bezier(mom) else 'FOLLOW_PATH'
regenerate = False
glyph_objects = []
for i, g in enumerate(text_properties.glyphs):
glyph_objects.append(g.glyph_object)
# use_path messes with parenting
# however, we need it for follow_path
# check if perhaps one glyph was deleted
if (type(g.glyph_object) == type(None)
or type(g.glyph_object.parent) == type(None)
or g.glyph_object.parent.users_collection != g.glyph_object.users_collection):
regenerate = True
elif len(text_properties.text) > i and g.glyph_id != text_properties.text[i]:
regenerate = True
elif len(text_properties.text) > i and (g.glyph_object[f"{utils.prefix()}_font_name"] != text_properties.font_name
or g.glyph_object[f"{utils.prefix()}_face_name"] != text_properties.face_name):
regenerate = True
if len(text_properties.text) != len(text_properties.glyphs):
regenerate = True
# blender bug
# https://projects.blender.org/blender/blender/issues/100661
previous_use_path = mom.data.use_path
if distribution_type == 'CALCULATE':
mom.data.use_path = False
elif distribution_type == 'FOLLOW_PATH':
mom.data.use_path = True
regenerate = will_regenerate(text_properties)
if mom.data.use_path:
regenerate = True
# if we regenerate.... delete objects
if regenerate and text_properties.get("glyphs"):
glyph_objects = [ g["glyph_object"] for g in text_properties["glyphs"] ]
completely_delete_objects(glyph_objects, True)
if regenerate:
completely_delete_objects(glyph_objects)
text_properties.glyphs.clear()
#TODO: fix selection with context_override
previous_selection = bpy.context.selected_objects if hasattr(bpy.context, "selected_objects") else [ o for o in bpy.context.scene.objects if o.select_get() ]
bpy.ops.object.select_all(action='DESELECT')
selected_objects = []
curve_length = get_curve_length(mom)
advance = text_properties.offset
glyph_advance = 0
@ -780,7 +747,7 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4)
glyph = Font.get_glyph(text_properties.font_name,
text_properties.face_name,
glyph_id).original
glyph_id)
if glyph == None:
# self.report({'ERROR'}, f"Glyph not found for {font_name} {face_name} {glyph_id}")
@ -788,20 +755,16 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4)
continue
ob = None
obg = None
if regenerate:
ob = bpy.data.objects.new(f"{glyph_id}", None)
obg = bpy.data.objects.new(f"{glyph_id}_mesh", glyph.data)
ob = bpy.data.objects.new(f"{glyph_id}", glyph.data)
ob[f"{utils.prefix()}_type"] = "glyph"
ob[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id
ob[f"{utils.prefix()}_font_name"] = text_properties.font_name
ob[f"{utils.prefix()}_face_name"] = text_properties.face_name
else:
ob = text_properties.glyphs[i].glyph_object
for c in ob.children:
if c.name.startswith(f"{glyph_id}_mesh"):
obg = c
distribution_type = 'CALCULATE' if is_bezier(mom) else 'FOLLOW_PATH'
if distribution_type == 'FOLLOW_PATH':
ob.constraints.new(type='FOLLOW_PATH')
ob.constraints["Follow Path"].target = mom
@ -810,7 +773,6 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4)
ob.constraints["Follow Path"].use_curve_follow = True
ob.constraints["Follow Path"].forward_axis = "FORWARD_X"
ob.constraints["Follow Path"].up_axis = "UP_Y"
spline_index = 0
elif distribution_type == 'CALCULATE':
location, tangent, spline_index = calc_point_on_bezier_curve(mom, advance, True, True)
if spline_index != previous_spline_index:
@ -818,11 +780,6 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4)
if regenerate:
ob.location = mom.matrix_world @ (location + text_properties.translation)
mom.users_collection[0].objects.link(obg)
mom.users_collection[0].objects.link(ob)
ob.parent = mom
obg.parent = ob
obg.location = mathutils.Vector((0.0, 0.0, 0.0))
else:
ob.location = (location + text_properties.translation)
@ -839,20 +796,16 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4)
local_main_axis)
if ob.rotation_mode != 'QUATERNION':
ob.rotation_mode = 'QUATERNION'
if obg.rotation_mode != 'QUATERNION':
obg.rotation_mode = 'QUATERNION'
q = mathutils.Quaternion()
q.rotate(text_properties.orientation)
if regenerate:
obg.rotation_quaternion = q
ob.rotation_quaternion = (mom.matrix_world @ motor[0]).to_quaternion()
ob.rotation_quaternion = (mom.matrix_world @ motor[0] @ q.to_matrix().to_4x4()).to_quaternion()
else:
ob.rotation_quaternion = motor[0].to_quaternion()
ob.rotation_quaternion = (motor[0] @ q.to_matrix().to_4x4()).to_quaternion()
else:
q = mathutils.Quaternion()
q.rotate(text_properties.orientation)
# obg.rotation_quaternion = q
obg.rotation_quaternion = (mom.matrix_world @ q.to_matrix().to_4x4()).to_quaternion()
ob.rotation_quaternion = q
# ob.rotation_quaternion = (mom.matrix_world @ q.to_matrix().to_4x4()).to_quaternion()
@ -886,12 +839,17 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4)
previous_spline_index = spline_index
if regenerate:
mom.users_collection[0].objects.link(ob)
glyph_data = text_properties.glyphs.add()
glyph_data.glyph_id = glyph_id
glyph_data.glyph_object = ob
glyph_data.letter_spacing = 0
ob.select_set(True)
if regenerate:
mom.select_set(True)
# https://projects.blender.org/blender/blender/issues/100661
mom.data.use_path = False
mom[f"{utils.prefix()}_type"] = "textobject"
mom[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id
mom[f"{utils.prefix()}_font_name"] = text_properties.font_name
@ -900,42 +858,10 @@ def set_text_on_curve(text_properties, reset_timeout_s=0.1, reset_depsgraph_n=4)
mom[f"{utils.prefix()}_letter_spacing"] = text_properties.letter_spacing
mom[f"{utils.prefix()}_orientation"] = text_properties.orientation
mom[f"{utils.prefix()}_translation"] = text_properties.translation
if "lock_depsgraph_update_ntimes" in bpy.context.scene.abc3d_data:
bpy.context.scene.abc3d_data["lock_depsgraph_update_ntimes"] += len(bpy.context.selected_objects)
else:
bpy.context.scene.abc3d_data["lock_depsgraph_update_ntimes"] = len(bpy.context.selected_objects)
# NOTE: we reset with a timeout, as setting and resetting certain things
# in fast succession will cause visual glitches (e.g. {}.data.use_path).
def reset():
mom.data.use_path = previous_use_path
if counted_reset in bpy.app.handlers.depsgraph_update_post:
bpy.app.handlers.depsgraph_update_post.remove(counted_reset)
if bpy.app.timers.is_registered(reset):
bpy.app.timers.unregister(reset)
molotov = reset_depsgraph_n + 0
def counted_reset(scene, depsgraph):
nonlocal molotov
if molotov == 0:
reset()
else:
molotov -= 1
# unregister previous resets to avoid multiple execution
if bpy.app.timers.is_registered(reset):
bpy.app.timers.unregister(reset)
if counted_reset in bpy.app.handlers.depsgraph_update_post:
bpy.app.handlers.depsgraph_update_post.remove(counted_reset)
if not isinstance(reset_timeout_s, bool):
if reset_timeout_s > 0:
bpy.app.timers.register(reset, first_interval=reset_timeout_s)
elif reset_timeout <= 0:
reset()
bpy.app.handlers.depsgraph_update_post.append(counted_reset)
bpy.context.view_layer.objects.active = mom
bpy.ops.object.parent_set(type='OBJECT')
bpy.context.scene.abc3d_data["lock_depsgraph_update_ntimes"] = len(bpy.context.selected_objects)
mom["lock_depsgraph_update_ntimes"] = len(bpy.context.selected_objects)
# endtime = time.perf_counter_ns()
# elapsedtime = endtime - starttime
@ -1169,12 +1095,6 @@ def get_metrics_object(o):
return c
return None
def get_original(o):
if hasattr(o, "original"):
return o.original
else:
return o
def add_default_metrics_to_objects(objects=None, overwrite_existing=False):
if type(objects) == type(None):
objects=bpy.context.selected_objects

View file

@ -120,6 +120,9 @@ class Font:
self.faces = faces
# 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({})

View file

@ -1,10 +1,10 @@
# NOTE: also change version in ../__init__.py
def get_version_major():
return 0
def get_version_minor():
return 0
def get_version_patch():
return 3
return 2
def get_version_string():
return f"{get_version_major()}.{get_version_minor()}.{get_version_patch}"
def prefix():