manual positioning

closes #1
This commit is contained in:
themancalledjakob 2024-12-07 14:57:33 +01:00
parent 35d864b9b8
commit 42c4a33801
3 changed files with 168 additions and 72 deletions

View file

@ -383,10 +383,10 @@ class ABC3D_PT_FontList(bpy.types.Panel):
row.scale_y = scale_y row.scale_y = scale_y
row.label(text=text) row.label(text=text)
row = layout.row() row = layout.row()
oper = row.operator(f"{__name__}.load_font", oper_lf = row.operator(f"{__name__}.load_font",
text='Load all glyphs in memory') text='Load all glyphs in memory')
oper.font_name = font_name oper_lf.font_name = font_name
oper.face_name = face_name oper_lf.face_name = face_name
class ABC3D_PT_TextPlacement(bpy.types.Panel): class ABC3D_PT_TextPlacement(bpy.types.Panel):
@ -914,6 +914,7 @@ class ABC3D_OT_RemoveText(bpy.types.Operator):
def delif(o, p): def delif(o, p):
if p in o: if p in o:
del o[p] del o[p]
delif(mom, f"{utils.prefix()}_type")
delif(mom, f"{utils.prefix()}_linked_textobject") delif(mom, f"{utils.prefix()}_linked_textobject")
delif(mom, f"{utils.prefix()}_font_name") delif(mom, f"{utils.prefix()}_font_name")
delif(mom, f"{utils.prefix()}_face_name") delif(mom, f"{utils.prefix()}_face_name")
@ -1046,7 +1047,7 @@ class ABC3D_OT_PlaceText(bpy.types.Operator):
# t.text) # t.text)
# or this: # or this:
# butils.set_text_on_curve(t) # butils.set_text_on_curve(t)
# else: else:
butils.ShowMessageBox( butils.ShowMessageBox(
title="No object selected", title="No object selected",
message=( message=(
@ -1372,7 +1373,7 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator):
font_name, font_name,
face_name, face_name,
glyph_id, glyph_id,
o) bpy.types.PointerProperty(o))
# TODO: is there a better way to iterate over a CollectionProperty? # TODO: is there a better way to iterate over a CollectionProperty?
found = False found = False
@ -1484,7 +1485,6 @@ def detect_text():
def load_used_glyphs(): def load_used_glyphs():
print("LOAD USED GLYPHS")
scene = bpy.context.scene scene = bpy.context.scene
abc3d_data = scene.abc3d_data abc3d_data = scene.abc3d_data
for t in abc3d_data.available_texts: for t in abc3d_data.available_texts:
@ -1528,19 +1528,33 @@ def on_frame_changed(self, dummy):
# TODO PERFORMANCE: only on demand # TODO PERFORMANCE: only on demand
butils.set_text_on_curve(t) 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 @persistent
def on_depsgraph_update(scene, depsgraph): def on_depsgraph_update(scene, depsgraph):
if not bpy.context.mode.startswith("EDIT"): global depsgraph_updates_locked
if not bpy.context.mode.startswith("EDIT") and not depsgraph_updates_locked:
for u in depsgraph.updates: for u in depsgraph.updates:
if f"{utils.prefix()}_linked_textobject" in u.id.keys() \ if f"{utils.prefix()}_linked_textobject" in u.id.keys() \
and f"{utils.prefix()}_type" in u.id.keys() \ and f"{utils.prefix()}_type" in u.id.keys() \
and u.id[f"{utils.prefix()}_type"] == 'textobject': and u.id[f"{utils.prefix()}_type"] == 'textobject':
linked_textobject = u.id[f"{utils.prefix()}_linked_textobject"] linked_textobject = u.id[f"{utils.prefix()}_linked_textobject"]
if u.is_updated_geometry and len(scene.abc3d_data.available_texts) > linked_textobject: if u.is_updated_geometry and len(scene.abc3d_data.available_texts) > linked_textobject:
lock_depsgraph_updates()
def later(): def later():
if not "lock_depsgraph_update_ntimes" in scene.abc3d_data \ 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( butils.set_text_on_curve(
scene.abc3d_data.available_texts[linked_textobject]) scene.abc3d_data.available_texts[linked_textobject])
elif scene.abc3d_data["lock_depsgraph_update_ntimes"] > 0: elif scene.abc3d_data["lock_depsgraph_update_ntimes"] > 0:
@ -1596,6 +1610,9 @@ def unregister():
if on_frame_changed in bpy.app.handlers.frame_change_post: if on_frame_changed in bpy.app.handlers.frame_change_post:
bpy.app.handlers.frame_change_post.remove(on_frame_changed) 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 del bpy.types.Scene.abc3d_data
print(f"UNREGISTER {bl_info['name']}") print(f"UNREGISTER {bl_info['name']}")

View file

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

204
butils.py
View file

@ -1,11 +1,12 @@
import bpy
import mathutils
import queue
import importlib import importlib
import os import os
import queue
import re import re
from multiprocessing import Process from multiprocessing import Process
import bpy
import mathutils
# import time # for debugging performance # import time # for debugging performance
# then import dependencies for our addon # then import dependencies for our addon
@ -111,7 +112,8 @@ def calc_tangent_on_bezier(bezier_point_1, bezier_point_2, t):
(-3 * (t**2) + 6 * t * (1 - t)) * h2 + (3 * t**2) * p2 (-3 * (t**2) + 6 * t * (1 - t)) * h2 + (3 * t**2) * p2
).normalized() ).normalized()
from math import radians, sqrt, pi, acos from math import acos, pi, radians, sqrt
def align_rotations_auto_pivot(mask, input_rotations, vectors, factors, local_main_axis): 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))] output_rotations = [mathutils.Matrix().to_3x3() for _ in range(len(input_rotations))]
@ -449,6 +451,7 @@ def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""):
glyph_obj = move_in_fontcollection( glyph_obj = move_in_fontcollection(
o, o,
fontcollection) fontcollection)
glyph_obj_pointer = bpy.types.PointerProperty(glyph_obj)
if glyph_obj == o: if glyph_obj == o:
del o[marker_property] del o[marker_property]
@ -457,7 +460,7 @@ def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""):
font_name, font_name,
face_name, face_name,
glyph_id, glyph_id,
glyph_obj) glyph_obj_pointer)
for c in o.children: for c in o.children:
if is_metrics_object(c): if is_metrics_object(c):
add_metrics_obj_from_bound_box(glyph_obj, add_metrics_obj_from_bound_box(glyph_obj,
@ -472,7 +475,7 @@ def load_font_from_filepath(filepath, glyphs="", font_name="", face_name=""):
for g in face.glyphs: for g in face.glyphs:
# iterate alternates # iterate alternates
for glyph in face.glyphs[g]: for glyph in face.glyphs[g]:
glyphs.append(glyph) glyphs.append(get_original(glyph))
if len(glyphs) > 0: if len(glyphs) > 0:
add_default_metrics_to_objects(glyphs) add_default_metrics_to_objects(glyphs)
# calculate unit factor # calculate unit factor
@ -586,18 +589,19 @@ def ShowMessageBox(title = "Message Box", icon = 'INFO', message=""):
bpy.context.window_manager.popup_menu(draw, title = title, icon = icon) bpy.context.window_manager.popup_menu(draw, title = title, icon = icon)
def simply_delete_objects(objs): def simply_delete_objects(objs):
context_override = bpy.context.copy() completely_delete_objects(objs)
context_override["selected_objects"] = list(objs)
with bpy.context.temp_override(**context_override):
bpy.ops.object.delete()
def completely_delete_objects(objs): def completely_delete_objects(objs, recursive=True):
simply_delete_objects(objs)
# remove deleted objects
# this is necessary
for g in objs: for g in objs:
if type(g) != type(None): 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: try:
bpy.data.objects.remove(g, do_unlink=True) bpy.data.objects.remove(g, do_unlink=True)
except ReferenceError as e: except ReferenceError as e:
@ -672,51 +676,80 @@ def prepare_text(font_name, face_name, text):
return True return True
def is_bezier(curve): def is_bezier(curve):
if curve.type != 'CURVE':
return False
if len(curve.data.splines) < 1: if len(curve.data.splines) < 1:
return False return False
return curve.data.splines[0].type == 'BEZIER' 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
"""
def set_text_on_curve(text_properties, recursive=True):
# starttime = time.perf_counter_ns() # starttime = time.perf_counter_ns()
mom = text_properties.text_object mom = text_properties.text_object
if mom.type != "CURVE": if mom.type != "CURVE":
return False return False
regenerate = False distribution_type = 'CALCULATE' if is_bezier(mom) else 'FOLLOW_PATH'
glyph_objects = []
for i, g in enumerate(text_properties.glyphs):
glyph_objects.append(g.glyph_object)
# check if perhaps one glyph was deleted # use_path messes with parenting
if (type(g.glyph_object) == type(None) # however, we need it for follow_path
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 # https://projects.blender.org/blender/blender/issues/100661
if mom.data.use_path: previous_use_path = mom.data.use_path
regenerate = True 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 we regenerate.... delete objects # if we regenerate.... delete objects
if regenerate: if regenerate and text_properties.get("glyphs"):
completely_delete_objects(glyph_objects) glyph_objects = [ g["glyph_object"] for g in text_properties["glyphs"] ]
completely_delete_objects(glyph_objects, True)
text_properties.glyphs.clear() 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) curve_length = get_curve_length(mom)
advance = text_properties.offset advance = text_properties.offset
glyph_advance = 0 glyph_advance = 0
@ -747,7 +780,7 @@ def set_text_on_curve(text_properties, recursive=True):
glyph = Font.get_glyph(text_properties.font_name, glyph = Font.get_glyph(text_properties.font_name,
text_properties.face_name, text_properties.face_name,
glyph_id) glyph_id).original
if glyph == None: if glyph == None:
# self.report({'ERROR'}, f"Glyph not found for {font_name} {face_name} {glyph_id}") # self.report({'ERROR'}, f"Glyph not found for {font_name} {face_name} {glyph_id}")
@ -755,16 +788,20 @@ def set_text_on_curve(text_properties, recursive=True):
continue continue
ob = None ob = None
obg = None
if regenerate: if regenerate:
ob = bpy.data.objects.new(f"{glyph_id}", glyph.data) ob = bpy.data.objects.new(f"{glyph_id}", None)
obg = bpy.data.objects.new(f"{glyph_id}_mesh", glyph.data)
ob[f"{utils.prefix()}_type"] = "glyph" ob[f"{utils.prefix()}_type"] = "glyph"
ob[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id ob[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id
ob[f"{utils.prefix()}_font_name"] = text_properties.font_name ob[f"{utils.prefix()}_font_name"] = text_properties.font_name
ob[f"{utils.prefix()}_face_name"] = text_properties.face_name ob[f"{utils.prefix()}_face_name"] = text_properties.face_name
else: else:
ob = text_properties.glyphs[i].glyph_object 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': if distribution_type == 'FOLLOW_PATH':
ob.constraints.new(type='FOLLOW_PATH') ob.constraints.new(type='FOLLOW_PATH')
ob.constraints["Follow Path"].target = mom ob.constraints["Follow Path"].target = mom
@ -773,6 +810,7 @@ def set_text_on_curve(text_properties, recursive=True):
ob.constraints["Follow Path"].use_curve_follow = True ob.constraints["Follow Path"].use_curve_follow = True
ob.constraints["Follow Path"].forward_axis = "FORWARD_X" ob.constraints["Follow Path"].forward_axis = "FORWARD_X"
ob.constraints["Follow Path"].up_axis = "UP_Y" ob.constraints["Follow Path"].up_axis = "UP_Y"
spline_index = 0
elif distribution_type == 'CALCULATE': elif distribution_type == 'CALCULATE':
location, tangent, spline_index = calc_point_on_bezier_curve(mom, advance, True, True) location, tangent, spline_index = calc_point_on_bezier_curve(mom, advance, True, True)
if spline_index != previous_spline_index: if spline_index != previous_spline_index:
@ -780,6 +818,11 @@ def set_text_on_curve(text_properties, recursive=True):
if regenerate: if regenerate:
ob.location = mom.matrix_world @ (location + text_properties.translation) 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: else:
ob.location = (location + text_properties.translation) ob.location = (location + text_properties.translation)
@ -796,16 +839,20 @@ def set_text_on_curve(text_properties, recursive=True):
local_main_axis) local_main_axis)
if ob.rotation_mode != 'QUATERNION': if ob.rotation_mode != 'QUATERNION':
ob.rotation_mode = 'QUATERNION' ob.rotation_mode = 'QUATERNION'
if obg.rotation_mode != 'QUATERNION':
obg.rotation_mode = 'QUATERNION'
q = mathutils.Quaternion() q = mathutils.Quaternion()
q.rotate(text_properties.orientation) q.rotate(text_properties.orientation)
if regenerate: if regenerate:
ob.rotation_quaternion = (mom.matrix_world @ motor[0] @ q.to_matrix().to_4x4()).to_quaternion() obg.rotation_quaternion = q
ob.rotation_quaternion = (mom.matrix_world @ motor[0]).to_quaternion()
else: else:
ob.rotation_quaternion = (motor[0] @ q.to_matrix().to_4x4()).to_quaternion() ob.rotation_quaternion = motor[0].to_quaternion()
else: else:
q = mathutils.Quaternion() q = mathutils.Quaternion()
q.rotate(text_properties.orientation) q.rotate(text_properties.orientation)
ob.rotation_quaternion = q # obg.rotation_quaternion = q
obg.rotation_quaternion = (mom.matrix_world @ q.to_matrix().to_4x4()).to_quaternion()
# ob.rotation_quaternion = (mom.matrix_world @ q.to_matrix().to_4x4()).to_quaternion() # ob.rotation_quaternion = (mom.matrix_world @ q.to_matrix().to_4x4()).to_quaternion()
@ -839,17 +886,12 @@ def set_text_on_curve(text_properties, recursive=True):
previous_spline_index = spline_index previous_spline_index = spline_index
if regenerate: if regenerate:
mom.users_collection[0].objects.link(ob)
glyph_data = text_properties.glyphs.add() glyph_data = text_properties.glyphs.add()
glyph_data.glyph_id = glyph_id glyph_data.glyph_id = glyph_id
glyph_data.glyph_object = ob glyph_data.glyph_object = ob
glyph_data.letter_spacing = 0 glyph_data.letter_spacing = 0
ob.select_set(True)
if regenerate: 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()}_type"] = "textobject"
mom[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id mom[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id
mom[f"{utils.prefix()}_font_name"] = text_properties.font_name mom[f"{utils.prefix()}_font_name"] = text_properties.font_name
@ -858,10 +900,42 @@ def set_text_on_curve(text_properties, recursive=True):
mom[f"{utils.prefix()}_letter_spacing"] = text_properties.letter_spacing mom[f"{utils.prefix()}_letter_spacing"] = text_properties.letter_spacing
mom[f"{utils.prefix()}_orientation"] = text_properties.orientation mom[f"{utils.prefix()}_orientation"] = text_properties.orientation
mom[f"{utils.prefix()}_translation"] = text_properties.translation mom[f"{utils.prefix()}_translation"] = text_properties.translation
bpy.context.view_layer.objects.active = mom
bpy.ops.object.parent_set(type='OBJECT') 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) bpy.context.scene.abc3d_data["lock_depsgraph_update_ntimes"] += len(bpy.context.selected_objects)
mom["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)
# endtime = time.perf_counter_ns() # endtime = time.perf_counter_ns()
# elapsedtime = endtime - starttime # elapsedtime = endtime - starttime
@ -1095,6 +1169,12 @@ def get_metrics_object(o):
return c return c
return None 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): def add_default_metrics_to_objects(objects=None, overwrite_existing=False):
if type(objects) == type(None): if type(objects) == type(None):
objects=bpy.context.selected_objects objects=bpy.context.selected_objects