Skip to content
271 changes: 156 additions & 115 deletions tt_operators.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,73 +10,35 @@
import bmesh
from mathutils import Vector
from mathutils.geometry import normal
from bpy.props import IntProperty, FloatProperty, StringProperty, BoolProperty

from bpy.props import (
IntProperty, FloatProperty, StringProperty, BoolProperty
)

def are_two_objects_in_editmode(objs):
if objs and len(objs) == 2:
if all((obj.type == "MESH" and obj.mode == "EDIT") for obj in objs):
return True

current_mode = {}
docstring = """select two verts or polygons only, then run this operator. selections can be on separate objects."""

docstring = """\
select two polygons only, then run this operator. polygons can be on separate objects."""

class TubeCallbackOps(bpy.types.Operator):

bl_idname = "object.tube_callback"
bl_label = "Tube Callback (private)"
bl_options = {"INTERNAL"}

current_name: StringProperty(default='')
fn: StringProperty(default='')
default: FloatProperty()

def dispatch(self, context, type_op):
wm = context.window_manager
operators = wm.operators

# only do this part if also current_name is passed in
if self.current_name:

cls = None
for k in operators:
if k.bl_idname == 'MESH_OT_add_curvebased_tube':
if k.generated_name == self.current_name:
cls = k

if not cls:
''' all callback functions require a valid class reference '''
return

if type_op == "Reset radii":
print('attempt reset:', cls.generated_name)
cls.main_scale = 1.0
cls.point1_scale = 1.0
cls.point2_scale = 1.0

elif type_op == "To Mesh":
cls.make_real()

else:
# would prefer to be implicit.. but self.default is OK for now.
# ideally, the value is derived from the prop default
# of cls.type_op. but for now it is passed explicitely.
# Barf. Dryheave.
setattr(cls, type_op, self.default)
cls.execute(context)

def execute(self, context):
self.dispatch(context, self.fn)
return {'FINISHED'}
def solve_mode_from_current_state():
descriptor = {
'objs': [],
'mode': None
}
in_edit_mode = bpy.context.edit_object
if not in_edit_mode:
return None

# can have multiple objects in edit mode.. i dont care how many
# but there can only be 2 sets of geometry selected at once.
num_objects_in_edit_mode = len(bpy.context.selected_objects)

def median(face):
return face.calc_center_median()

def avg_edge_length_of_connected_edges(v):
if not v.link_edges:
return 0
lengths = [e.calc_length() for e in v.link_edges]
return sum(lengths) / len(lengths)


def get_medians_and_normals(oper, context, mode):
"""
because this is a post hoc implementation to cater for 2.8 ability to multi-select objects in
Expand Down Expand Up @@ -131,6 +93,56 @@ def get_medians_and_normals(oper, context, mode):
op2_scale = scale2 / bevel_depth
extra_data = bevel_depth, scale2, op2_scale

elif mode == "THREE":
# single object, two verts
obj_main = bpy.context.edit_object
me = obj_main.data
bm = bmesh.from_edit_mesh(me)
# verts = []
avg_edge_length = []
for v in bm.verts:
if len(medians) > 2:
break
if v.select:
# verts.append(v)
avg_edge_length.append(avg_edge_length_of_connected_edges(v))
normals.append(v.normal)
medians.append(v.co)

# use v.link_edges , (average length)/2 of all link_edges
bevel_depth = avg_edge_length[0] / 2
scale2 = avg_edge_length[1] / 2
op2_scale = scale2 / bevel_depth
extra_data = bevel_depth, scale2, op2_scale

elif mode == "FOUR":
# two objects, single vert each
obj_one = bpy.context.selected_objects[0]
obj_two = bpy.context.selected_objects[1]
verts = []
avg_edge_length = []
objs = [obj_two, obj_one] if oper.flip_u else [obj_one, obj_two]
for obj in objs:

if len(medians) > 2:
break

m = obj.matrix_world
bm = bmesh.from_edit_mesh(obj.data)
for v in bm.verts:
if v.select:
verts.append(v)
avg_edge_length.append(avg_edge_length_of_connected_edges(v))
normals.append(v.normal) # not sure why this has weird results with "m @ v.normal"
medians.append(m @ v.co)
break

bevel_depth = avg_edge_length[0] / 2
scale2 = avg_edge_length[1] / 2
op2_scale = scale2 / bevel_depth
extra_data = bevel_depth, scale2, op2_scale


return medians, normals, extra_data


Expand Down Expand Up @@ -167,8 +179,6 @@ def modify_curve(medians, normals, curvename):

pointA, pointB = [0, -1] if not oper.flip_v else [-1, 0]

''' the radii stuff must be tidier before merge to master. '''

# Point 0
''' default scale or radius point1 == 1 '''
point1 = polyline.bezier_points[pointA]
Expand Down Expand Up @@ -199,6 +209,12 @@ def modify_curve(medians, normals, curvename):
# print('generated name:', generated_name)
modify_curve(medians, normals, generated_name)

def updateOperator(self, context, origin):
if getattr(self, origin):
setattr(self, origin, False)
prop_name = origin.replace("reset_", "")
self.property_unset(prop_name)
update_simple_tube(self, context)

class AddSimpleTube(bpy.types.Operator):

Expand Down Expand Up @@ -233,11 +249,37 @@ class AddSimpleTube(bpy.types.Operator):
flip_u: BoolProperty()

equal_radii: BoolProperty(default=0)
# joined = BoolProperty(default=0)

do_not_process: BoolProperty(default=False)
initialized_curve: BoolProperty(default=False)

reset_handle_ext_1: BoolProperty(default=False, update=lambda s, c: updateOperator(s, c, "reset_handle_ext_1"))
reset_point1_scale: BoolProperty(default=False, update=lambda s, c: updateOperator(s, c, "reset_point1_scale"))
reset_point2_scale: BoolProperty(default=False, update=lambda s, c: updateOperator(s, c, "reset_point2_scale"))
reset_handle_ext_2: BoolProperty(default=False, update=lambda s, c: updateOperator(s, c, "reset_handle_ext_2"))

def updateDelegation(self, context):
if self.trigger_bool_make_real:
self.trigger_bool_make_real = False
self.make_real()

def updateResetRadii(self, context):
if self.trigger_bool_reset_radii:
self.trigger_bool_reset_radii = False
print('attempt reset:', self.generated_name)
self.main_scale = 1.0
self.point1_scale = 1.0
self.point2_scale = 1.0

trigger_bool_make_real: BoolProperty(default=False, description="Make the tube final", update=updateDelegation)
trigger_bool_reset_radii: BoolProperty(default=False, description="Reset all radii and scale", update=updateResetRadii)

def are_two_objects_in_editmode(self):
objs = bpy.context.selected_objects
if objs and len(objs) == 2:
if all((obj.type == "MESH" and obj.mode == "EDIT") for obj in objs):
return True

def draw(self, context):
layout = self.layout
callback = "object.tube_callback"
Expand All @@ -253,26 +295,24 @@ def draw(self, context):

col.separator()

def prop_n_reset(split, pname, pstr, default, enabled=True):
def prop_n_reset(split, pname, display_name, enabled=True):
''' I draw a slider and an operator to reset the slider '''
pid = split.row(align=True)
pid.enabled = enabled
pid.prop(self, pname, text=pstr)
a = pid.operator(callback, text="", icon="LINKED")
a.fn = pname
a.current_name = self.generated_name
a.default = default
pid.prop(self, pname, text=display_name)
pid.prop(self, "reset_" + pname, text="", icon="LINKED")


er = not self.equal_radii
# ROW 1
row = col.row(); split = row.split(factor=0.5)
prop_n_reset(split, "handle_ext_1", "handle 1", 2.0) # left
prop_n_reset(split, "point1_scale", "radius_1", 1.0, er) # right
prop_n_reset(split, "handle_ext_1", "handle 1") # left
prop_n_reset(split, "point1_scale", "radius 1", er) # right

# ROW 2
row = col.row(); split = row.split()
prop_n_reset(split, "handle_ext_2", "handle 2", 2.0) # left
prop_n_reset(split, "point2_scale", "radius_2", 1.0, er) # right
prop_n_reset(split, "handle_ext_2", "handle 2") # left
prop_n_reset(split, "point2_scale", "radius 2", er) # right

# next row
row = layout.row()
Expand All @@ -291,53 +331,53 @@ def prop_n_reset(split, pname, pstr, default, enabled=True):
right_row.prop(self, "flip_v", text='Normal', toggle=True)

col = layout.column()

k = col.operator(callback, text="Reset radii")
k.fn = "Reset radii"
k.current_name = self.generated_name

k = col.operator(callback, text="To Mesh")
k.fn = 'To Mesh'
k.current_name = self.generated_name

# k = col.operator(callback, text="Join")
# k.fn = 'Join'
# k.current_name = self.generated_name
col.prop(self, "trigger_bool_reset_radii", text="Reset radii", toggle=True)
# col.prop(self, "trigger_bool_make_real", text="To Mesh", toggle=True)

def initialize_new_tube(self, context):
'''

"""
- create curve
- assign default values
- add to scene
- record given name
'''
"""

scn = bpy.context.scene
obj_main = bpy.context.edit_object
objects_main = bpy.context.selected_objects if are_two_objects_in_editmode(bpy.context.selected_objects) else None
objects_main = bpy.context.selected_objects if self.are_two_objects_in_editmode() else None

self_id = hash(self)

current_mode[self_id] = None

if obj_main and not objects_main:
if not (obj_main.data.total_face_sel == 2):
# if face mode and single object
if (obj_main.data.total_face_sel == 2):
mw = obj_main.matrix_world
current_mode[self_id] = "ONE"
elif (obj_main.data.total_vert_sel == 2):
mw = obj_main.matrix_world
current_mode[self_id] = "THREE"
else:
self.do_not_process = True
self.report({'WARNING'}, 'if only one object is selected, then select two faces only')
self.report({'WARNING'}, 'if only one object is selected, then select two faces or verts only')
return
current_mode[self_id] = "ONE"
mw = obj_main.matrix_world

elif objects_main:
if not all((obj.data.total_face_sel == 1) for obj in objects_main):

if all((obj.data.total_face_sel == 1) for obj in objects_main):
current_mode[self_id] = "TWO"
elif all((obj.data.total_vert_sel == 1) for obj in objects_main):
current_mode[self_id] = "FOUR"
else:
self.do_not_process = True
self.report({'WARNING'}, 'if two objects are selected, then select one face on each object')
return
current_mode[self_id] = "TWO"
else:
self.report({'WARNING'}, 'if one object in edit mode, pick 2 faces only. if two objects in edit mode, pick 1 face on each.')
msg = 'if one object in edit mode, pick 2 faces/verts only. if two objects in edit mode, pick 1 face/vertex on each.'
self.report({'WARNING'}, msg)
return



curvedata = bpy.data.curves.new(name=self.base_name, type='CURVE')
curvedata.dimensions = '3D'

Expand All @@ -347,7 +387,7 @@ def initialize_new_tube(self, context):
self.generated_name = obj.name
print(':::', self.generated_name, current_mode)

if current_mode[self_id] == "ONE":
if current_mode[self_id] in {"ONE", "THREE"}:
obj.matrix_world = mw.copy()

polyline = curvedata.splines.new('BEZIER')
Expand All @@ -357,29 +397,31 @@ def initialize_new_tube(self, context):


@classmethod
def poll(self, context):
def poll(cls, context):
# return self.do_not_process
obj = bpy.context.edit_object
if obj and obj.data.total_face_sel == 2:
print(f"faces={obj.data.total_face_sel}, verts={obj.data.total_vert_sel}")
if obj and (obj.data.total_face_sel == 2 or obj.data.total_vert_sel == 2):
return True

return are_two_objects_in_editmode(bpy.context.selected_objects)
# return AddSimpleTube.are_two_objects_in_editmode(None) # __class__.are_two_objects_in_editmode(None) ?
return __class__.are_two_objects_in_editmode(None)

# def make_real(self):
# objects = bpy.data.objects
# obj = objects[self.generated_name] # this curve object

def make_real(self):
objects = bpy.data.objects
obj = objects[self.generated_name] # this curve object
# obj_data = obj.to_mesh() # bpy.context.depsgraph, apply_modifiers=modifiers, calc_undeformed=settings)

settings = False
modifiers = True
obj_data = obj.to_mesh() # bpy.context.depsgraph, apply_modifiers=modifiers, calc_undeformed=settings)
# obj_n = objects.new('MESHED_' + obj.name, obj_data)
# obj_n.location = (0, 0, 0)
# obj_n.matrix_world = obj.matrix_world.copy()
# bpy.context.collection.objects.link(obj_n)
# # bpy.ops.object.convert(target='MESH')

obj_n = objects.new('MESHED_' + obj.name, obj_data)
obj_n.location = (0, 0, 0)
obj_n.matrix_world = obj.matrix_world.copy()
bpy.context.collection.objects.link(obj_n)
obj.hide_render = True
obj.hide_viewport = True
# return obj_n
# obj.hide_render = True
# obj.hide_viewport = True
# return obj_n

def execute(self, context):

Expand All @@ -391,8 +433,7 @@ def execute(self, context):
return {'FINISHED'}


TubeCallbackOps.__doc__ = docstring
AddSimpleTube.__doc__ = docstring

classes = [TubeCallbackOps, AddSimpleTube]
classes = [AddSimpleTube]
register, unregister = bpy.utils.register_classes_factory(classes)