diff --git a/tt_operators.py b/tt_operators.py index c400500..2674599 100644 --- a/tt_operators.py +++ b/tt_operators.py @@ -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 @@ -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 @@ -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] @@ -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): @@ -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" @@ -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() @@ -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' @@ -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') @@ -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): @@ -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)