def execute(self, context): if not (maplus_geom.get_active_object() and maplus_geom.get_select_state(maplus_geom.get_active_object())): self.report( {'ERROR'}, ('Cannot complete: need at least' ' one active (and selected) object.') ) return {'CANCELLED'} addon_data = bpy.context.scene.maplus_data prims = addon_data.prim_list previous_mode = maplus_geom.get_active_object().mode if not hasattr(self, "quick_op_target"): active_item = prims[addon_data.active_list_item] else: active_item = addon_data.quick_directional_slide_transf if (maplus_geom.get_active_object() and type(maplus_geom.get_active_object().data) == bpy.types.Mesh): if not hasattr(self, "quick_op_target"): if prims[active_item.ds_direction].kind != 'LINE': self.report( {'ERROR'}, 'Wrong operand: "Directional Slide" can' ' only operate on a line' ) return {'CANCELLED'} # a bmesh can only be initialized in edit mode... if previous_mode != 'EDIT': bpy.ops.object.editmode_toggle() else: # else we could already be in edit mode with some stale # updates, exiting and reentering forces an update bpy.ops.object.editmode_toggle() bpy.ops.object.editmode_toggle() # Get global coordinate data for each geometry item, with # modifiers applied. Grab either directly from the scene data # (for quick ops), or from the MAPlus primitives # CollectionProperty on the scene data (for advanced tools) if hasattr(self, 'quick_op_target'): if addon_data.quick_directional_slide_auto_grab_src: vert_attribs_to_set = ('line_start', 'line_end') try: vert_data = maplus_geom.return_selected_verts( maplus_geom.get_active_object(), len(vert_attribs_to_set), maplus_geom.get_active_object().matrix_world ) except maplus_except.InsufficientSelectionError: self.report({'ERROR'}, 'Not enough vertices selected.') return {'CANCELLED'} except maplus_except.NonMeshGrabError: self.report( {'ERROR'}, 'Cannot grab coords: non-mesh or no active object.' ) return {'CANCELLED'} maplus_geom.set_item_coords( addon_data.quick_directional_slide_src, vert_attribs_to_set, vert_data ) src_global_data = maplus_geom.get_modified_global_coords( geometry=addon_data.quick_directional_slide_src, kind='LINE' ) else: src_global_data = maplus_geom.get_modified_global_coords( geometry=prims[active_item.ds_direction], kind='LINE' ) # These global point coordinate vectors will be used to construct # geometry and transformations in both object (global) space # and mesh (local) space dir_start = src_global_data[0] dir_end = src_global_data[1] # create common vars needed for object and for mesh level transfs active_obj_transf = maplus_geom.get_active_object().matrix_world.copy() inverse_active = active_obj_transf.copy() inverse_active.invert() multi_edit_targets = [ model for model in bpy.context.scene.objects if ( maplus_geom.get_select_state(model) and model.type == 'MESH' ) ] if self.target == 'OBJECT': for item in multi_edit_targets: # Make the vector specifying the direction and # magnitude to slide in direction = dir_end - dir_start # Take modifiers on the transformation item into account, # in global (object) space if active_item.ds_make_unit_vec: direction.normalize() if active_item.ds_flip_direction: direction.negate() direction *= active_item.ds_multiplier item.location += direction else: for item in multi_edit_targets: self.report( {'WARNING'}, ('Warning/Experimental: mesh transforms' ' on objects with non-uniform scaling' ' are not currently supported.') ) # Init source mesh src_mesh = bmesh.new() src_mesh.from_mesh(item.data) # Get the object world matrix item_matrix_unaltered_loc = item.matrix_world.copy() unaltered_inverse_loc = item_matrix_unaltered_loc.copy() unaltered_inverse_loc.invert() # Stored geom data in local coords dir_start_loc = unaltered_inverse_loc @ dir_start dir_end_loc = unaltered_inverse_loc @ dir_end # Get translation vector in local space direction_loc = dir_end_loc - dir_start_loc # Take modifiers on the transformation item into account, # in local (mesh) space if active_item.ds_make_unit_vec: # There are special considerations for this modifier # since we need to achieve a global length of # one, but can only transform it in local space # (NOTE: assumes only uniform scaling on the # active object) scaling_factor = 1.0 / item.scale[0] direction_loc.normalize() direction_loc *= scaling_factor if active_item.ds_flip_direction: direction_loc.negate() direction_loc *= active_item.ds_multiplier dir_slide = mathutils.Matrix.Translation(direction_loc) if self.target == 'MESHSELECTED': src_mesh.transform( dir_slide, filter={'SELECT'} ) elif self.target == 'WHOLEMESH': src_mesh.transform(dir_slide) # write and then release the mesh data bpy.ops.object.mode_set(mode='OBJECT') src_mesh.to_mesh(item.data) src_mesh.free() # Go back to whatever mode we were in before doing this bpy.ops.object.mode_set(mode=previous_mode) else: self.report( {'ERROR'}, 'Cannot transform: non-mesh or no active object.' ) return {'CANCELLED'} return {'FINISHED'}
def execute(self, context): if not (maplus_geom.get_active_object() and maplus_geom.get_select_state(maplus_geom.get_active_object())): self.report({'ERROR'}, ('Cannot complete: need at least one active' ' (and selected) object.')) return {'CANCELLED'} addon_data = bpy.context.scene.maplus_data prims = addon_data.prim_list previous_mode = maplus_geom.get_active_object().mode if hasattr(self, "quick_op_target"): active_item = addon_data.quick_scale_match_edge_transf else: active_item = prims[addon_data.active_list_item] if (maplus_geom.get_active_object() and type( maplus_geom.get_active_object().data) == bpy.types.Mesh): if not hasattr(self, "quick_op_target"): if (prims[active_item.sme_edge_one].kind != 'LINE' or prims[active_item.sme_edge_two].kind != 'LINE'): self.report({'ERROR'}, ('Wrong operands: "Scale Match Edge" can only' ' operate on two lines')) return {'CANCELLED'} if previous_mode != 'EDIT': bpy.ops.object.editmode_toggle() else: # else we could already be in edit mode with some stale # updates, exiting and reentering forces an update bpy.ops.object.editmode_toggle() bpy.ops.object.editmode_toggle() # Get global coordinate data for each geometry item, with # applicable modifiers applied. Grab either (A) directly from # the scene data (for quick ops), (B) from the MAPlus primitives # CollectionProperty on the scene data (for advanced tools), or # (C) from the selected verts directly for numeric input mode if hasattr(self, "quick_op_target"): # Numeric mode is part of this op's quick tools if addon_data.quick_sme_numeric_mode: if addon_data.quick_sme_numeric_auto: vert_attribs_to_set = ('line_start', 'line_end') try: vert_data = maplus_geom.return_selected_verts( maplus_geom.get_active_object(), len(vert_attribs_to_set), maplus_geom.get_active_object().matrix_world) except maplus_except.InsufficientSelectionError: self.report({'ERROR'}, 'Not enough vertices selected.') return {'CANCELLED'} except maplus_except.NonMeshGrabError: self.report({'ERROR'}, ('Cannot grab coords: non-mesh' ' or no active object.')) return {'CANCELLED'} maplus_geom.set_item_coords( addon_data.quick_sme_numeric_src, vert_attribs_to_set, vert_data) maplus_geom.set_item_coords( addon_data.quick_sme_numeric_dest, vert_attribs_to_set, vert_data) addon_data.quick_sme_numeric_dest.ln_make_unit_vec = (True) addon_data.quick_sme_numeric_dest.ln_multiplier = ( addon_data.quick_sme_numeric_length) src_global_data = maplus_geom.get_modified_global_coords( geometry=addon_data.quick_sme_numeric_src, kind='LINE') dest_global_data = maplus_geom.get_modified_global_coords( geometry=addon_data.quick_sme_numeric_dest, kind='LINE') # Non-numeric (normal quick op) mode else: if addon_data.quick_scale_match_edge_auto_grab_src: vert_attribs_to_set = ('line_start', 'line_end') try: vert_data = maplus_geom.return_selected_verts( maplus_geom.get_active_object(), len(vert_attribs_to_set), maplus_geom.get_active_object().matrix_world) except maplus_except.InsufficientSelectionError: self.report({'ERROR'}, 'Not enough vertices selected.') return {'CANCELLED'} except maplus_except.NonMeshGrabError: self.report({'ERROR'}, ('Cannot grab coords: non-mesh' ' or no active object.')) return {'CANCELLED'} maplus_geom.set_item_coords( addon_data.quick_scale_match_edge_src, vert_attribs_to_set, vert_data) src_global_data = maplus_geom.get_modified_global_coords( geometry=addon_data.quick_scale_match_edge_src, kind='LINE') dest_global_data = maplus_geom.get_modified_global_coords( geometry=addon_data.quick_scale_match_edge_dest, kind='LINE') # Else, operate on data from the advanced tools else: src_global_data = maplus_geom.get_modified_global_coords( geometry=prims[active_item.sme_edge_one], kind='LINE') dest_global_data = maplus_geom.get_modified_global_coords( geometry=prims[active_item.sme_edge_two], kind='LINE') # These global point coordinate vectors will be used to construct # geometry and transformations in both object (global) space # and mesh (local) space src_start = src_global_data[0] src_end = src_global_data[1] dest_start = dest_global_data[0] dest_end = dest_global_data[1] # create common vars needed for object and for mesh # level transforms active_obj_transf = maplus_geom.get_active_object( ).matrix_world.copy() inverse_active = active_obj_transf.copy() inverse_active.invert() # Construct vectors for each edge from the global point coord data src_edge = src_end - src_start dest_edge = dest_end - dest_start if dest_edge.length == 0 or src_edge.length == 0: self.report( {'ERROR'}, 'Divide by zero error: zero length edge encountered') return {'CANCELLED'} scale_factor = dest_edge.length / src_edge.length multi_edit_targets = [ model for model in bpy.context.scene.objects if (maplus_geom.get_select_state(model) and model.type == 'MESH') ] if self.target == 'OBJECT': for item in multi_edit_targets: # Get the object world matrix before we modify it here item_matrix_unaltered = item.matrix_world.copy() unaltered_inverse = item_matrix_unaltered.copy() unaltered_inverse.invert() # (Note that there are no transformation modifiers for this # transformation type, so that section is omitted here) item.scale = [scale_factor * num for num in item.scale] bpy.context.view_layer.update() # put the original line starting point (before the object # was transformed) into the local object space src_pivot_location_local = unaltered_inverse @ src_start # get final global position of pivot (source line # start coords) after object rotation new_global_src_pivot_coords = ( item.matrix_world @ src_pivot_location_local) # get translation, new to old (original) pivot location new_to_old_pivot = (src_start - new_global_src_pivot_coords) item.location = (item.location + new_to_old_pivot) bpy.context.view_layer.update() else: for item in multi_edit_targets: # (Note that there are no transformation modifiers for this # transformation type, so that section is omitted here) self.report({'WARNING'}, ('Warning/Experimental: mesh transforms' ' on objects with non-uniform scaling' ' are not currently supported.')) # Init source mesh src_mesh = bmesh.new() src_mesh.from_mesh(item.data) item_matrix_unaltered_loc = item.matrix_world.copy() unaltered_inverse_loc = item_matrix_unaltered_loc.copy() unaltered_inverse_loc.invert() # Stored geom data in local coords src_start_loc = unaltered_inverse_loc @ src_start src_end_loc = unaltered_inverse_loc @ src_end dest_start_loc = unaltered_inverse_loc @ dest_start dest_end_loc = unaltered_inverse_loc @ dest_end # Construct vectors for each line in local space loc_src_line = src_end_loc - src_start_loc loc_dest_line = dest_end_loc - dest_start_loc # Get the scale match matrix scaling_match = mathutils.Matrix.Scale(scale_factor, 4) # Get the new pivot location new_pivot_location_loc = scaling_match @ src_start_loc # Get the translation, new to old pivot location new_to_old_pivot_vec = (src_start_loc - new_pivot_location_loc) new_to_old_pivot = mathutils.Matrix.Translation( new_to_old_pivot_vec) # Get combined scale + move match_transf = new_to_old_pivot @ scaling_match if self.target == 'MESHSELECTED': src_mesh.transform(match_transf, filter={'SELECT'}) elif self.target == 'WHOLEMESH': src_mesh.transform(match_transf) # write and then release the mesh data bpy.ops.object.mode_set(mode='OBJECT') src_mesh.to_mesh(item.data) src_mesh.free() # Go back to whatever mode we were in before doing this bpy.ops.object.mode_set(mode=previous_mode) else: self.report({'ERROR'}, 'Cannot transform: non-mesh or no active object.') return {'CANCELLED'} return {'FINISHED'}
def execute(self, context): if not (maplus_geom.get_active_object() and maplus_geom.get_select_state(maplus_geom.get_active_object())): self.report( {'ERROR'}, ('Cannot complete: need at least' ' one active (and selected) object.') ) return {'CANCELLED'} addon_data = bpy.context.scene.maplus_data prims = addon_data.prim_list previous_mode = maplus_geom.get_active_object().mode if not hasattr(self, "quick_op_target"): active_item = prims[addon_data.active_list_item] else: active_item = addon_data.quick_align_planes_transf if (maplus_geom.get_active_object() and type(maplus_geom.get_active_object().data) == bpy.types.Mesh): if not hasattr(self, "quick_op_target"): if (prims[active_item.apl_src_plane].kind != 'PLANE' or prims[active_item.apl_dest_plane].kind != 'PLANE'): self.report( {'ERROR'}, ('Wrong operands: "Align Planes" can only operate on ' 'two planes') ) return {'CANCELLED'} # a bmesh can only be initialized in edit mode... if previous_mode != 'EDIT': bpy.ops.object.editmode_toggle() else: # else we could already be in edit mode with some stale # updates, exiting and reentering forces an update bpy.ops.object.editmode_toggle() bpy.ops.object.editmode_toggle() # Get global coordinate data for each geometry item, with # modifiers applied. Grab either directly from the scene data # (for quick ops), or from the MAPlus primitives # CollectionProperty on the scene data (for advanced tools) if hasattr(self, "quick_op_target"): if addon_data.quick_align_planes_auto_grab_src: vert_attribs_to_set = ( 'plane_pt_a', 'plane_pt_b', 'plane_pt_c' ) try: vert_data = maplus_geom.return_selected_verts( maplus_geom.get_active_object(), len(vert_attribs_to_set), maplus_geom.get_active_object().matrix_world ) except maplus_except.InsufficientSelectionError: self.report({'ERROR'}, 'Not enough vertices selected.') return {'CANCELLED'} except maplus_except.NonMeshGrabError: self.report( {'ERROR'}, 'Cannot grab coords: non-mesh or no active object.' ) return {'CANCELLED'} maplus_geom.set_item_coords( addon_data.quick_align_planes_src, vert_attribs_to_set, vert_data ) src_global_data = maplus_geom.get_modified_global_coords( geometry=addon_data.quick_align_planes_src, kind='PLANE' ) dest_global_data = maplus_geom.get_modified_global_coords( geometry=addon_data.quick_align_planes_dest, kind='PLANE' ) else: src_global_data = maplus_geom.get_modified_global_coords( geometry=prims[active_item.apl_src_plane], kind='PLANE' ) dest_global_data = maplus_geom.get_modified_global_coords( geometry=prims[active_item.apl_dest_plane], kind='PLANE' ) # These global point coordinate vectors will be used to construct # geometry and transformations in both object (global) space # and mesh (local) space if active_item.apl_alternate_pivot: src_pt_a = src_global_data[1] src_pt_b = src_global_data[0] else: src_pt_a = src_global_data[0] src_pt_b = src_global_data[1] src_pt_c = src_global_data[2] if active_item.apl_alternate_pivot: dest_pt_a = dest_global_data[1] dest_pt_b = dest_global_data[0] else: dest_pt_a = dest_global_data[0] dest_pt_b = dest_global_data[1] dest_pt_c = dest_global_data[2] # create common vars needed for object and for mesh level transfs active_obj_transf = maplus_geom.get_active_object().matrix_world.copy() inverse_active = active_obj_transf.copy() inverse_active.invert() # We need global data for the object operation and for creation # of a custom transform orientation if the user enables it. # construct normal vector for first (source) plane src_pln_ln_BA = src_pt_a - src_pt_b src_pln_ln_BC = src_pt_c - src_pt_b src_normal = src_pln_ln_BA.cross(src_pln_ln_BC) # Take modifiers on the transformation item into account, # in global (object) space if active_item.apl_flip_normal: src_normal.negate() # construct normal vector for second (destination) plane dest_pln_ln_BA = dest_pt_a - dest_pt_b dest_pln_ln_BC = dest_pt_c - dest_pt_b dest_normal = dest_pln_ln_BA.cross(dest_pln_ln_BC) # find rotational difference between source and dest planes rotational_diff = src_normal.rotation_difference(dest_normal) # Set up edge alignment (BA plane1 to BA plane2) new_lead_edge_orientation = src_pln_ln_BA.copy() new_lead_edge_orientation.rotate(rotational_diff) parallelize_edges = new_lead_edge_orientation.rotation_difference( dest_pln_ln_BA ) # TODO: Disabled until Blender 2.8 custom transform # orientations status is known # # Create custom transform orientation, for sliding the user's # # target along the destination face after it has been aligned. # # We do this by making a basis matrix out of the dest plane # # leading edge vector, the dest normal vector, and the cross # # of those two (each vector is normalized first) # vdest = dest_pln_ln_BA.copy() # vdest.normalize() # vnorm = dest_normal.copy() # vnorm.normalize() # # vnorm.negate() # vcross = vdest.cross(vnorm) # vcross.normalize() # vcross.negate() # custom_orientation = mathutils.Matrix( # [ # [vcross[0], vnorm[0], vdest[0]], # [vcross[1], vnorm[1], vdest[1]], # [vcross[2], vnorm[2], vdest[2]] # ] # ) # bpy.ops.transform.create_orientation( # name='MAPlus', # use=active_item.apl_use_custom_orientation, # overwrite=True # ) # bpy.context.scene.orientations['MAPlus'].matrix = ( # custom_orientation # ) multi_edit_targets = [ model for model in bpy.context.scene.objects if ( maplus_geom.get_select_state(model) and model.type == 'MESH' ) ] if self.target == 'OBJECT': for item in multi_edit_targets: # Get the object world matrix before we modify it here item_matrix_unaltered = item.matrix_world.copy() unaltered_inverse = item_matrix_unaltered.copy() unaltered_inverse.invert() # Try to rotate the object by the rotational_diff item.rotation_euler.rotate( rotational_diff ) bpy.context.view_layer.update() # Parallelize the leading edges item.rotation_euler.rotate( parallelize_edges ) bpy.context.view_layer.update() # get local coords using active object as basis, in # other words, determine coords of the source pivot # relative to the active object's origin by reversing # the active object's transf from the pivot's coords local_src_pivot_coords = ( unaltered_inverse @ src_pt_b ) # find the new global location of the pivot (we access # the item's matrix_world directly here since we # changed/updated it earlier) new_global_src_pivot_coords = ( item.matrix_world @ local_src_pivot_coords ) # figure out how to translate the object (the translation # vector) so that the source pivot sits on the destination # pivot's location # first vector is the global/absolute distance # between the two pivots pivot_to_dest = ( dest_pt_b - new_global_src_pivot_coords ) item.location = ( item.location + pivot_to_dest ) bpy.context.view_layer.update() else: for item in multi_edit_targets: self.report( {'WARNING'}, ('Warning/Experimental: mesh transforms' ' on objects with non-uniform scaling' ' are not currently supported.') ) src_mesh = bmesh.new() src_mesh.from_mesh(item.data) item_matrix_unaltered_loc = item.matrix_world.copy() unaltered_inverse_loc = item_matrix_unaltered_loc.copy() unaltered_inverse_loc.invert() # Stored geom data in local coords src_a_loc = unaltered_inverse_loc @ src_pt_a src_b_loc = unaltered_inverse_loc @ src_pt_b src_c_loc = unaltered_inverse_loc @ src_pt_c dest_a_loc = unaltered_inverse_loc @ dest_pt_a dest_b_loc = unaltered_inverse_loc @ dest_pt_b dest_c_loc = unaltered_inverse_loc @ dest_pt_c src_ba_loc = src_a_loc - src_b_loc src_bc_loc = src_c_loc - src_b_loc src_normal_loc = src_ba_loc.cross(src_bc_loc) # Take modifiers on the transformation item into account, # in local (mesh) space if active_item.apl_flip_normal: src_normal_loc.negate() dest_ba_loc = dest_a_loc - dest_b_loc dest_bc_loc = dest_c_loc - dest_b_loc dest_normal_loc = dest_ba_loc.cross(dest_bc_loc) # Get translation, move source pivot to local origin src_b_inv = src_b_loc.copy() src_b_inv.negate() src_pivot_to_loc_origin = mathutils.Matrix.Translation( src_b_inv ) # Get rotational diff between planes loc_rot_diff = src_normal_loc.rotation_difference( dest_normal_loc ) parallelize_planes_loc = loc_rot_diff.to_matrix() parallelize_planes_loc.resize_4x4() # Get edge alignment rotation (align leading plane edges) new_lead_edge_ornt_loc = ( parallelize_planes_loc @ src_ba_loc ) edge_align_loc = ( new_lead_edge_ornt_loc.rotation_difference( dest_ba_loc ) ) parallelize_edges_loc = edge_align_loc.to_matrix() parallelize_edges_loc.resize_4x4() # Get translation, move pivot to destination pivot_to_dest_loc = mathutils.Matrix.Translation( dest_b_loc ) mesh_coplanar = ( pivot_to_dest_loc @ parallelize_edges_loc @ parallelize_planes_loc @ src_pivot_to_loc_origin ) if self.target == 'MESHSELECTED': src_mesh.transform( mesh_coplanar, filter={'SELECT'} ) elif self.target == 'WHOLEMESH': src_mesh.transform(mesh_coplanar) bpy.ops.object.mode_set(mode='OBJECT') src_mesh.to_mesh(item.data) # Go back to whatever mode we were in before doing this bpy.ops.object.mode_set(mode=previous_mode) else: self.report( {'ERROR'}, "\nCannot transform: non-mesh or no active object." ) return {'CANCELLED'} return {'FINISHED'}
def execute(self, context): addon_data = bpy.context.scene.maplus_data prims = addon_data.prim_list previous_mode = maplus_geom.get_active_object().mode if not hasattr(self, "quick_op_target"): active_item = prims[addon_data.active_list_item] else: active_item = addon_data.quick_align_pts_transf # Gather selected Blender object(s) to apply the transform to multi_edit_targets = [ item for item in bpy.context.scene.objects if ( maplus_geom.get_select_state(item) ) ] # Check prerequisites for mesh level transforms, need an active/selected object if (self.target != 'OBJECT' and not (maplus_geom.get_active_object() and maplus_geom.get_select_state(maplus_geom.get_active_object()))): self.report( {'ERROR'}, ('Cannot complete: cannot perform mesh-level transform' ' without an active (and selected) object.') ) return {'CANCELLED'} # Check auto grab prerequisites if addon_data.quick_align_pts_auto_grab_src: if not (maplus_geom.get_active_object() and maplus_geom.get_select_state(maplus_geom.get_active_object())): self.report( {'ERROR'}, ('Cannot complete: cannot auto-grab source verts ' ' without an active (and selected) object.') ) return {'CANCELLED'} if maplus_geom.get_active_object().type != 'MESH': self.report( {'ERROR'}, ('Cannot complete: cannot auto-grab source verts ' ' from a non-mesh object.') ) return {'CANCELLED'} # Proceed only if selected Blender objects are compatible with the transform target # (Do not allow mesh-level transforms when there are non-mesh objects selected) if not (self.target in {'MESH_SELECTED', 'WHOLE_MESH', 'OBJECT_ORIGIN'} and [item for item in multi_edit_targets if item.type != 'MESH']): # todo: use a bool check and put on all derived classes # instead of hasattr if not hasattr(self, 'quick_op_target'): if (prims[active_item.apt_pt_one].kind != 'POINT' or prims[active_item.apt_pt_two].kind != 'POINT'): self.report( {'ERROR'}, ('Wrong operands: "Align Points" can only operate on ' 'two points') ) return {'CANCELLED'} if maplus_geom.get_active_object().type == 'MESH': # a bmesh can only be initialized in edit mode...todo/better way? if previous_mode != 'EDIT': bpy.ops.object.editmode_toggle() else: # else we could already be in edit mode with some stale # updates, exiting and reentering forces an update bpy.ops.object.editmode_toggle() bpy.ops.object.editmode_toggle() # Get global coordinate data for each geometry item, with # modifiers applied. Grab either directly from the scene data # (for quick ops), or from the MAPlus primitives # CollectionProperty on the scene data (for advanced tools) if hasattr(self, 'quick_op_target'): if addon_data.quick_align_pts_auto_grab_src: vert_attribs_to_set = ('point',) try: vert_data = maplus_geom.return_selected_verts( maplus_geom.get_active_object(), len(vert_attribs_to_set), maplus_geom.get_active_object().matrix_world ) except maplus_except.InsufficientSelectionError: self.report({'ERROR'}, 'Not enough vertices selected.') return {'CANCELLED'} except maplus_except.NonMeshGrabError: self.report( {'ERROR'}, 'Cannot grab coords: non-mesh or no active object.' ) return {'CANCELLED'} maplus_geom.set_item_coords( addon_data.quick_align_pts_src, vert_attribs_to_set, vert_data ) src_global_data = maplus_geom.get_modified_global_coords( geometry=addon_data.quick_align_pts_src, kind='POINT' ) dest_global_data = maplus_geom.get_modified_global_coords( geometry=addon_data.quick_align_pts_dest, kind='POINT' ) else: src_global_data = maplus_geom.get_modified_global_coords( geometry=prims[active_item.apt_pt_one], kind='POINT' ) dest_global_data = maplus_geom.get_modified_global_coords( geometry=prims[active_item.apt_pt_two], kind='POINT' ) # These global point coordinate vectors will be used to construct # geometry and transformations in both object (global) space # and mesh (local) space src_pt = src_global_data[0] dest_pt = dest_global_data[0] if self.target in {'OBJECT', 'OBJECT_ORIGIN'}: for item in multi_edit_targets: align_points = dest_pt - src_pt # Take modifiers on the transformation item into account, # in global (object) space if active_item.apt_make_unit_vector: align_points.normalize() if active_item.apt_flip_direction: align_points.negate() align_points *= active_item.apt_multiplier item.location += align_points if self.target in {'MESH_SELECTED', 'WHOLE_MESH', 'OBJECT_ORIGIN'}: for item in multi_edit_targets: self.report( {'WARNING'}, ('Warning/Experimental: mesh transforms' ' on objects with non-uniform scaling' ' are not currently supported.') ) # Init source mesh src_mesh = bmesh.new() src_mesh.from_mesh(item.data) active_obj_transf = maplus_geom.get_active_object().matrix_world.copy() inverse_active = active_obj_transf.copy() inverse_active.invert() # Stored geom data in local coords src_pt_loc = inverse_active @ src_pt dest_pt_loc = inverse_active @ dest_pt # Get translation vector (in local space), src to dest align_points_vec = dest_pt_loc - src_pt_loc # Take modifiers on the transformation item into account, # in local (mesh) space if active_item.apt_make_unit_vector: # There are special considerations for this modifier # since we need to achieve a global length of # one, but can only transform it in local space # (NOTE: assumes only uniform scaling on the # active object) scaling_factor = 1.0 / item.scale[0] align_points_vec.normalize() align_points_vec *= scaling_factor if active_item.apt_flip_direction: align_points_vec.negate() align_points_vec *= active_item.apt_multiplier align_points_loc = mathutils.Matrix.Translation( align_points_vec ) if self.target == 'MESH_SELECTED': src_mesh.transform( align_points_loc, filter={'SELECT'} ) elif self.target == 'WHOLE_MESH': src_mesh.transform(align_points_loc) elif self.target == 'OBJECT_ORIGIN': # Note: a target of 'OBJECT_ORIGIN' is equivalent # to performing an object transf. + an inverse # whole mesh level transf. To the user, # the object appears to stay in the same place, # while only the object's origin moves. src_mesh.transform(align_points_loc.inverted()) # write and then release the mesh data bpy.ops.object.mode_set(mode='OBJECT') src_mesh.to_mesh(item.data) src_mesh.free() # Go back to whatever mode we were in before doing this bpy.ops.object.mode_set(mode=previous_mode) else: # The selected Blender objects are not compatible with the # requested transformation type (we can't apply a transform # to mesh data when there are non-mesh objects selected) self.report( {'ERROR'}, ('Cannot complete: Cannot apply mesh-level' ' transformations to selected non-mesh objects.') ) return {'CANCELLED'} return {'FINISHED'}
def execute(self, context): addon_data = bpy.context.scene.maplus_data prims = addon_data.prim_list previous_mode = maplus_geom.get_active_object().mode if hasattr(self, "quick_op_target"): active_item = addon_data.quick_align_lines_transf else: active_item = prims[addon_data.active_list_item] # Gather selected Blender object(s) to apply the transform to multi_edit_targets = [ item for item in bpy.context.scene.objects if (maplus_geom.get_select_state(item)) ] # Check prerequisites for mesh level transforms, need an active/selected object if (self.target != 'OBJECT' and not (maplus_geom.get_active_object() and maplus_geom.get_select_state( maplus_geom.get_active_object()))): self.report({'ERROR'}, ('Cannot complete: cannot perform mesh-level transform' ' without an active (and selected) object.')) return {'CANCELLED'} # Check auto grab prerequisites if addon_data.quick_align_lines_auto_grab_src: if not (maplus_geom.get_active_object() and maplus_geom.get_select_state( maplus_geom.get_active_object())): self.report({'ERROR'}, ('Cannot complete: cannot auto-grab source verts ' ' without an active (and selected) object.')) return {'CANCELLED'} if maplus_geom.get_active_object().type != 'MESH': self.report({'ERROR'}, ('Cannot complete: cannot auto-grab source verts ' ' from a non-mesh object.')) return {'CANCELLED'} # Proceed only if selected Blender objects are compatible with the transform target # (Do not allow mesh-level transforms when there are non-mesh objects selected) if not (self.target in { 'MESH_SELECTED', 'WHOLE_MESH', 'OBJECT_ORIGIN' } and [item for item in multi_edit_targets if item.type != 'MESH']): if not hasattr(self, "quick_op_target"): if (prims[active_item.aln_src_line].kind != 'LINE' or prims[active_item.aln_dest_line].kind != 'LINE'): self.report( {'ERROR'}, ('Wrong operands: "Align Lines" can only operate on ' 'two lines')) return {'CANCELLED'} if maplus_geom.get_active_object().type == 'MESH': # a bmesh can only be initialized in edit mode... if previous_mode != 'EDIT': bpy.ops.object.editmode_toggle() else: # else we could already be in edit mode with some stale # updates, exiting and reentering forces an update bpy.ops.object.editmode_toggle() bpy.ops.object.editmode_toggle() # Get global coordinate data for each geometry item, with # modifiers applied. Grab either directly from the scene data # (for quick ops), or from the MAPlus primitives # CollectionProperty on the scene data (for advanced tools) if hasattr(self, 'quick_op_target'): if addon_data.quick_align_lines_auto_grab_src: vert_attribs_to_set = ('line_start', 'line_end') try: vert_data = maplus_geom.return_selected_verts( maplus_geom.get_active_object(), len(vert_attribs_to_set), maplus_geom.get_active_object().matrix_world) except maplus_except.InsufficientSelectionError: self.report({'ERROR'}, 'Not enough vertices selected.') return {'CANCELLED'} except maplus_except.NonMeshGrabError: self.report({ 'ERROR' }, 'Cannot grab coords: non-mesh or no active object.') return {'CANCELLED'} maplus_geom.set_item_coords( addon_data.quick_align_lines_src, vert_attribs_to_set, vert_data) src_global_data = maplus_geom.get_modified_global_coords( geometry=addon_data.quick_align_lines_src, kind='LINE') dest_global_data = maplus_geom.get_modified_global_coords( geometry=addon_data.quick_align_lines_dest, kind='LINE') else: src_global_data = maplus_geom.get_modified_global_coords( geometry=prims[active_item.aln_src_line], kind='LINE') dest_global_data = maplus_geom.get_modified_global_coords( geometry=prims[active_item.aln_dest_line], kind='LINE') # These global point coordinate vectors will be used to construct # geometry and transformations in both object (global) space # and mesh (local) space src_start = src_global_data[0] src_end = src_global_data[1] dest_start = dest_global_data[0] dest_end = dest_global_data[1] if self.target in {'OBJECT', 'OBJECT_ORIGIN'}: for item in multi_edit_targets: # Get the object world matrix before we modify it here item_matrix_unaltered = item.matrix_world.copy() unaltered_inverse = item_matrix_unaltered.copy() unaltered_inverse.invert() # construct lines from the stored geometry src_line = src_end - src_start dest_line = dest_end - dest_start # Take modifiers on the transformation item into account, # in global (object) space if active_item.aln_flip_direction: src_line.negate() # find rotational difference between source and dest lines rotational_diff = src_line.rotation_difference(dest_line) parallelize_lines = rotational_diff.to_matrix() parallelize_lines.resize_4x4() # rotate active object so line one is parallel linear, # position will be corrected after this item.rotation_euler.rotate(rotational_diff) bpy.context.view_layer.update() # put the original line starting point (before the object # was rotated) into the local object space src_pivot_location_local = unaltered_inverse @ src_start # get final global position of pivot (source line # start coords) after object rotation new_global_src_pivot_coords = ( item.matrix_world @ src_pivot_location_local) # get translation, pivot to dest pivot_to_dest = (dest_start - new_global_src_pivot_coords) item.location = (item.location + pivot_to_dest) bpy.context.view_layer.update() if self.target in {'MESH_SELECTED', 'WHOLE_MESH', 'OBJECT_ORIGIN'}: for item in multi_edit_targets: self.report({'WARNING'}, ('Warning/Experimental: mesh transforms' ' on objects with non-uniform scaling' ' are not currently supported.')) # Init source mesh src_mesh = bmesh.new() src_mesh.from_mesh(item.data) # Get the object world matrix item_matrix_unaltered_loc = item.matrix_world.copy() unaltered_inverse_loc = item_matrix_unaltered_loc.copy() unaltered_inverse_loc.invert() # Stored geom data in local coords src_start_loc = unaltered_inverse_loc @ src_start src_end_loc = unaltered_inverse_loc @ src_end dest_start_loc = unaltered_inverse_loc @ dest_start dest_end_loc = unaltered_inverse_loc @ dest_end # Construct vectors for each line in local space loc_src_line = src_end_loc - src_start_loc loc_dest_line = dest_end_loc - dest_start_loc # Take modifiers on the transformation item into account, # in local (mesh) space if active_item.aln_flip_direction: loc_src_line.negate() # Get translation, move source pivot to local origin src_start_inv = src_start_loc.copy() src_start_inv.negate() src_pivot_to_loc_origin = mathutils.Matrix.Translation( src_start_inv) # Get edge alignment rotation (align src to dest) loc_rdiff = loc_src_line.rotation_difference(loc_dest_line) parallelize_lines_loc = loc_rdiff.to_matrix() parallelize_lines_loc.resize_4x4() # Get translation, move pivot to destination pivot_to_dest_loc = mathutils.Matrix.Translation( dest_start_loc) loc_make_collinear = ( pivot_to_dest_loc @ parallelize_lines_loc @ src_pivot_to_loc_origin) if self.target == 'MESH_SELECTED': src_mesh.transform(loc_make_collinear, filter={'SELECT'}) elif self.target == 'WHOLE_MESH': src_mesh.transform(loc_make_collinear) elif self.target == 'OBJECT_ORIGIN': # Note: a target of 'OBJECT_ORIGIN' is equivalent # to performing an object transf. + an inverse # whole mesh level transf. To the user, # the object appears to stay in the same place, # while only the object's origin moves. src_mesh.transform(loc_make_collinear.inverted()) bpy.ops.object.mode_set(mode='OBJECT') src_mesh.to_mesh(item.data) src_mesh.free() # Go back to whatever mode we were in before doing this bpy.ops.object.mode_set(mode=previous_mode) else: # The selected Blender objects are not compatible with the # requested transformation type (we can't apply a transform # to mesh data when there are non-mesh objects selected) self.report({'ERROR'}, ('Cannot complete: Cannot apply mesh-level' ' transformations to selected non-mesh objects.')) return {'CANCELLED'} return {'FINISHED'}
def execute(self, context): addon_data = bpy.context.scene.maplus_data prims = addon_data.prim_list previous_mode = maplus_geom.get_active_object().mode if not hasattr(self, "quick_op_target"): active_item = prims[addon_data.active_list_item] else: active_item = addon_data.quick_align_planes_transf # Gather selected Blender object(s) to apply the transform to multi_edit_targets = [ item for item in bpy.context.scene.objects if (maplus_geom.get_select_state(item)) ] # Check prerequisites for mesh level transforms, need an active/selected object if (self.target != 'OBJECT' and not (maplus_geom.get_active_object() and maplus_geom.get_select_state( maplus_geom.get_active_object()))): self.report({'ERROR'}, ('Cannot complete: cannot perform mesh-level transform' ' without an active (and selected) object.')) return {'CANCELLED'} # Check auto grab prerequisites if addon_data.quick_align_planes_auto_grab_src: if not (maplus_geom.get_active_object() and maplus_geom.get_select_state( maplus_geom.get_active_object())): self.report({'ERROR'}, ('Cannot complete: cannot auto-grab source verts ' ' without an active (and selected) object.')) return {'CANCELLED'} if maplus_geom.get_active_object().type != 'MESH': self.report({'ERROR'}, ('Cannot complete: cannot auto-grab source verts ' ' from a non-mesh object.')) return {'CANCELLED'} # Proceed only if selected Blender objects are compatible with the transform target # (Do not allow mesh-level transforms when there are non-mesh objects selected) if not (self.target in { 'MESH_SELECTED', 'WHOLE_MESH', 'OBJECT_ORIGIN' } and [item for item in multi_edit_targets if item.type != 'MESH']): if not hasattr(self, "quick_op_target"): if (prims[active_item.apl_src_plane].kind != 'PLANE' or prims[active_item.apl_dest_plane].kind != 'PLANE'): self.report( {'ERROR'}, ('Wrong operands: "Align Planes" can only operate on ' 'two planes')) return {'CANCELLED'} if maplus_geom.get_active_object().type == 'MESH': # a bmesh can only be initialized in edit mode... if previous_mode != 'EDIT': bpy.ops.object.editmode_toggle() else: # else we could already be in edit mode with some stale # updates, exiting and reentering forces an update bpy.ops.object.editmode_toggle() bpy.ops.object.editmode_toggle() # Get global coordinate data for each geometry item, with # modifiers applied. Grab either directly from the scene data # (for quick ops), or from the MAPlus primitives # CollectionProperty on the scene data (for advanced tools) if hasattr(self, "quick_op_target"): if (addon_data.quick_align_planes_auto_grab_src and not addon_data.quick_align_planes_set_origin_mode): vert_attribs_to_set = ('plane_pt_a', 'plane_pt_b', 'plane_pt_c') try: vert_data = maplus_geom.return_selected_verts( maplus_geom.get_active_object(), len(vert_attribs_to_set), maplus_geom.get_active_object().matrix_world) except maplus_except.InsufficientSelectionError: self.report({'ERROR'}, 'Not enough vertices selected.') return {'CANCELLED'} except maplus_except.NonMeshGrabError: self.report({ 'ERROR' }, 'Cannot grab coords: non-mesh or no active object.') return {'CANCELLED'} maplus_geom.set_item_coords( addon_data.quick_align_planes_src, vert_attribs_to_set, vert_data) src_global_data = maplus_geom.get_modified_global_coords( geometry=addon_data.quick_align_planes_src, kind='PLANE') dest_global_data = maplus_geom.get_modified_global_coords( geometry=addon_data.quick_align_planes_dest, kind='PLANE') else: src_global_data = maplus_geom.get_modified_global_coords( geometry=prims[active_item.apl_src_plane], kind='PLANE') dest_global_data = maplus_geom.get_modified_global_coords( geometry=prims[active_item.apl_dest_plane], kind='PLANE') # These global point coordinate vectors will be used to construct # geometry and transformations in both object (global) space # and mesh (local) space if active_item.apl_alternate_pivot: src_pt_a = src_global_data[1] src_pt_b = src_global_data[0] else: src_pt_a = src_global_data[0] src_pt_b = src_global_data[1] src_pt_c = src_global_data[2] if active_item.apl_alternate_pivot: dest_pt_a = dest_global_data[1] dest_pt_b = dest_global_data[0] else: dest_pt_a = dest_global_data[0] dest_pt_b = dest_global_data[1] dest_pt_c = dest_global_data[2] # We need global data for the object operation and for creation # of a custom transform orientation if the user enables it. # construct normal vector for first (source) plane src_pln_ln_BA = src_pt_a - src_pt_b src_pln_ln_BC = src_pt_c - src_pt_b src_normal = src_pln_ln_BA.cross(src_pln_ln_BC) # Take modifiers on the transformation item into account, # in global (object) space if active_item.apl_flip_normal: src_normal.negate() # construct normal vector for second (destination) plane dest_pln_ln_BA = dest_pt_a - dest_pt_b dest_pln_ln_BC = dest_pt_c - dest_pt_b dest_normal = dest_pln_ln_BA.cross(dest_pln_ln_BC) # find rotational difference between source and dest planes rotational_diff = src_normal.rotation_difference(dest_normal) # Set up edge alignment (BA plane1 to BA plane2) new_lead_edge_orientation = src_pln_ln_BA.copy() new_lead_edge_orientation.rotate(rotational_diff) parallelize_edges = new_lead_edge_orientation.rotation_difference( dest_pln_ln_BA) # Create custom transform orientation, for sliding the user's # target along the destination face after it has been aligned. # We do this by making a basis matrix out of the dest plane # leading edge vector, the dest normal vector, and the cross # of those two (each vector is normalized first) vdest = dest_pln_ln_BA.copy() vdest.normalize() vnorm = dest_normal.copy() vnorm.normalize() # vnorm.negate() vcross = vdest.cross(vnorm) vcross.normalize() vcross.negate() orthonormal_basis_matrix = mathutils.Matrix( [[vcross[0], vnorm[0], vdest[0]], [vcross[1], vnorm[1], vdest[1]], [vcross[2], vnorm[2], vdest[2]]]) bpy.ops.transform.create_orientation( name='MAPlus', use=active_item.apl_use_custom_orientation, overwrite=True) bpy.context.view_layer.update() orient_slot = [ slot for slot in bpy.context.scene.transform_orientation_slots if slot.custom_orientation and slot.custom_orientation.name == 'MAPlus' ] if orient_slot: orient_slot[ 0].custom_orientation.matrix = orthonormal_basis_matrix else: print('Error: Could not find MAPlus transform orientation...') if hasattr(self, 'quick_op_target' ) and addon_data.quick_align_planes_set_origin_mode: # TODO: Refactor this feature or possibly make it a new full operator # This entire block is for *Set Origin* mode. It is equivalent to an # OBJECT_ORIGIN transformation (both OBJECT and WHOLE_MESH transforms, # with the mesh level transf. inverted), with a special set of SOURCE # verts (a triangle at the current object's origin per object) for item in multi_edit_targets: ######## COMMON DATA ######## # *Set Origin* mode uses a set of 3 pts at the object's origin src_pt_a = (item.matrix_world @ mathutils.Vector( (1, 0.0, 0.0))) src_pt_b = (item.matrix_world @ mathutils.Vector( (0.0, 0.0, 0.0))) src_pt_c = (item.matrix_world @ mathutils.Vector( (0.0, 1, 0.0))) # We have a separate/alternate storage plane for this data dest_data_set_origin_mode = maplus_geom.get_modified_global_coords( geometry=addon_data. quick_align_planes_set_origin_mode_dest, kind='PLANE') dest_pt_a = dest_data_set_origin_mode[0] dest_pt_b = dest_data_set_origin_mode[1] dest_pt_c = dest_data_set_origin_mode[2] # Set the pivot point here (co-located points on src/dest after alignment) src_pivot = src_pt_b dest_pivot = dest_pt_b if addon_data.quick_align_planes_set_origin_mode_alt_pivot: print('AAA') # *Set Origin* mode uses a set of 3 pts at the object's origin src_pt_a, src_pt_b = src_pt_b, src_pt_a src_pivot = src_pt_a dest_pivot = dest_pt_a # We need global data for the object operation and for creation # of a custom transform orientation if the user enables it. # construct normal vector for first (source) plane src_pln_ln_BA = src_pt_a - src_pt_b src_pln_ln_BC = src_pt_c - src_pt_b src_normal = src_pln_ln_BA.cross(src_pln_ln_BC) # construct normal vector for second (destination) plane dest_pln_ln_BA = dest_pt_a - dest_pt_b dest_pln_ln_BC = dest_pt_c - dest_pt_b dest_normal = dest_pln_ln_BA.cross(dest_pln_ln_BC) # find rotational difference between source and dest planes rotational_diff = src_normal.rotation_difference( dest_normal) # Set up edge alignment (BA plane1 to BA plane2) new_lead_edge_orientation = src_pln_ln_BA.copy() new_lead_edge_orientation.rotate(rotational_diff) parallelize_edges = new_lead_edge_orientation.rotation_difference( dest_pln_ln_BA) ######## OBJECT ######## # Get the object world matrix before we modify it here item_matrix_unaltered = item.matrix_world.copy() unaltered_inverse = item_matrix_unaltered.copy() unaltered_inverse.invert() # Try to rotate the object by the rotational_diff item.rotation_euler.rotate(rotational_diff) bpy.context.view_layer.update() # Parallelize the leading edges item.rotation_euler.rotate(parallelize_edges) bpy.context.view_layer.update() # get local coords using active object as basis, in # other words, determine coords of the source pivot # relative to the active object's origin by reversing # the active object's transf from the pivot's coords local_src_pivot_coords = (unaltered_inverse @ src_pivot) # find the new global location of the pivot (we access # the item's matrix_world directly here since we # changed/updated it earlier) new_global_src_pivot_coords = ( item.matrix_world @ local_src_pivot_coords) # figure out how to translate the object (the translation # vector) so that the source pivot sits on the destination # pivot's location # first vector is the global/absolute distance # between the two pivots pivot_to_dest = (dest_pivot - new_global_src_pivot_coords) item.location = (item.location + pivot_to_dest) bpy.context.view_layer.update() ######## MESH ######## self.report({'WARNING'}, ('Warning/Experimental: mesh transforms' ' on objects with non-uniform scaling' ' are not currently supported.')) src_mesh = bmesh.new() src_mesh.from_mesh(item.data) item_matrix_unaltered_loc = item.matrix_world.copy() unaltered_inverse_loc = item_matrix_unaltered_loc.copy() unaltered_inverse_loc.invert() # Stored geom data in local coords src_a_loc = unaltered_inverse_loc @ src_pt_a src_b_loc = unaltered_inverse_loc @ src_pt_b src_c_loc = unaltered_inverse_loc @ src_pt_c dest_a_loc = unaltered_inverse_loc @ dest_pt_a dest_b_loc = unaltered_inverse_loc @ dest_pt_b dest_c_loc = unaltered_inverse_loc @ dest_pt_c src_ba_loc = src_a_loc - src_b_loc src_bc_loc = src_c_loc - src_b_loc src_normal_loc = src_ba_loc.cross(src_bc_loc) dest_ba_loc = dest_a_loc - dest_b_loc dest_bc_loc = dest_c_loc - dest_b_loc dest_normal_loc = dest_ba_loc.cross(dest_bc_loc) # Set the pivot point here (co-located points on src/dest after alignment) src_pivot_loc = src_b_loc dest_pivot_loc = dest_b_loc if addon_data.quick_align_planes_set_origin_mode_alt_pivot: # *Set Origin* mode uses a set of 3 pts at the object's origin print('BBB') src_pivot_loc = src_a_loc dest_pivot_loc = dest_a_loc # Get translation, move source pivot to local origin src_pivot_inv = src_pivot_loc.copy() src_pivot_inv.negate() src_pivot_to_loc_origin = mathutils.Matrix.Translation( src_pivot_inv) # Get rotational diff between planes loc_rot_diff = src_normal_loc.rotation_difference( dest_normal_loc) parallelize_planes_loc = loc_rot_diff.to_matrix() parallelize_planes_loc.resize_4x4() # Get edge alignment rotation (align leading plane edges) new_lead_edge_ornt_loc = ( parallelize_planes_loc @ src_ba_loc) edge_align_loc = (new_lead_edge_ornt_loc. rotation_difference(dest_ba_loc)) parallelize_edges_loc = edge_align_loc.to_matrix() parallelize_edges_loc.resize_4x4() # Get translation, move pivot to destination pivot_to_dest_loc = mathutils.Matrix.Translation( dest_pivot_loc) mesh_coplanar = ( pivot_to_dest_loc @ parallelize_edges_loc @ parallelize_planes_loc @ src_pivot_to_loc_origin) # Special *Set Origin* mode needs only a # mesh level OBJECT_ORIGIN transform only src_mesh.transform(mesh_coplanar.inverted()) bpy.ops.object.mode_set(mode='OBJECT') src_mesh.to_mesh(item.data) else: if self.target in {'OBJECT', 'OBJECT_ORIGIN'}: for item in multi_edit_targets: # Get the object world matrix before we modify it here item_matrix_unaltered = item.matrix_world.copy() unaltered_inverse = item_matrix_unaltered.copy() unaltered_inverse.invert() # Try to rotate the object by the rotational_diff item.rotation_euler.rotate(rotational_diff) bpy.context.view_layer.update() # Parallelize the leading edges item.rotation_euler.rotate(parallelize_edges) bpy.context.view_layer.update() # get local coords using active object as basis, in # other words, determine coords of the source pivot # relative to the active object's origin by reversing # the active object's transf from the pivot's coords local_src_pivot_coords = (unaltered_inverse @ src_pt_b) # find the new global location of the pivot (we access # the item's matrix_world directly here since we # changed/updated it earlier) new_global_src_pivot_coords = ( item.matrix_world @ local_src_pivot_coords) # figure out how to translate the object (the translation # vector) so that the source pivot sits on the destination # pivot's location # first vector is the global/absolute distance # between the two pivots pivot_to_dest = (dest_pt_b - new_global_src_pivot_coords) item.location = (item.location + pivot_to_dest) bpy.context.view_layer.update() if self.target in { 'MESH_SELECTED', 'WHOLE_MESH', 'OBJECT_ORIGIN' }: for item in multi_edit_targets: self.report({'WARNING'}, ('Warning/Experimental: mesh transforms' ' on objects with non-uniform scaling' ' are not currently supported.')) src_mesh = bmesh.new() src_mesh.from_mesh(item.data) item_matrix_unaltered_loc = item.matrix_world.copy() unaltered_inverse_loc = item_matrix_unaltered_loc.copy( ) unaltered_inverse_loc.invert() # Stored geom data in local coords src_a_loc = unaltered_inverse_loc @ src_pt_a src_b_loc = unaltered_inverse_loc @ src_pt_b src_c_loc = unaltered_inverse_loc @ src_pt_c dest_a_loc = unaltered_inverse_loc @ dest_pt_a dest_b_loc = unaltered_inverse_loc @ dest_pt_b dest_c_loc = unaltered_inverse_loc @ dest_pt_c src_ba_loc = src_a_loc - src_b_loc src_bc_loc = src_c_loc - src_b_loc src_normal_loc = src_ba_loc.cross(src_bc_loc) # Take modifiers on the transformation item into account, # in local (mesh) space if active_item.apl_flip_normal: src_normal_loc.negate() dest_ba_loc = dest_a_loc - dest_b_loc dest_bc_loc = dest_c_loc - dest_b_loc dest_normal_loc = dest_ba_loc.cross(dest_bc_loc) # Get translation, move source pivot to local origin src_b_inv = src_b_loc.copy() src_b_inv.negate() src_pivot_to_loc_origin = mathutils.Matrix.Translation( src_b_inv) # Get rotational diff between planes loc_rot_diff = src_normal_loc.rotation_difference( dest_normal_loc) parallelize_planes_loc = loc_rot_diff.to_matrix() parallelize_planes_loc.resize_4x4() # Get edge alignment rotation (align leading plane edges) new_lead_edge_ornt_loc = ( parallelize_planes_loc @ src_ba_loc) edge_align_loc = (new_lead_edge_ornt_loc. rotation_difference(dest_ba_loc)) parallelize_edges_loc = edge_align_loc.to_matrix() parallelize_edges_loc.resize_4x4() # Get translation, move pivot to destination pivot_to_dest_loc = mathutils.Matrix.Translation( dest_b_loc) mesh_coplanar = ( pivot_to_dest_loc @ parallelize_edges_loc @ parallelize_planes_loc @ src_pivot_to_loc_origin) if self.target == 'MESH_SELECTED': src_mesh.transform(mesh_coplanar, filter={'SELECT'}) elif self.target == 'WHOLE_MESH': src_mesh.transform(mesh_coplanar) elif self.target == 'OBJECT_ORIGIN': # Note: a target of 'OBJECT_ORIGIN' is equivalent # to performing an object transf. + an inverse # whole mesh level transf. To the user, # the object appears to stay in the same place, # while only the object's origin moves. src_mesh.transform(mesh_coplanar.inverted()) bpy.ops.object.mode_set(mode='OBJECT') src_mesh.to_mesh(item.data) # Go back to whatever mode we were in before doing this bpy.ops.object.mode_set(mode=previous_mode) else: # The selected Blender objects are not compatible with the # requested transformation type (we can't apply a transform # to mesh data when there are non-mesh objects selected) self.report({'ERROR'}, ('Cannot complete: Cannot apply mesh-level' ' transformations to selected non-mesh objects.')) return {'CANCELLED'} return {'FINISHED'}
def execute(self, context): addon_data = bpy.context.scene.maplus_data prims = addon_data.prim_list previous_mode = maplus_geom.get_active_object().mode if not hasattr(self, "quick_op_target"): active_item = prims[addon_data.active_list_item] else: active_item = addon_data.quick_axis_rotate_transf # Gather selected Blender object(s) to apply the transform to multi_edit_targets = [ item for item in bpy.context.scene.objects if (maplus_geom.get_select_state(item)) ] # Check prerequisites for mesh level transforms, need an active/selected object if (self.target != 'OBJECT' and not (maplus_geom.get_active_object() and maplus_geom.get_select_state( maplus_geom.get_active_object()))): self.report({'ERROR'}, ('Cannot complete: cannot perform mesh-level transform' ' without an active (and selected) object.')) return {'CANCELLED'} # Check auto grab prerequisites if addon_data.quick_axis_rotate_auto_grab_src: if not (maplus_geom.get_active_object() and maplus_geom.get_select_state( maplus_geom.get_active_object())): self.report({'ERROR'}, ('Cannot complete: cannot auto-grab source verts ' ' without an active (and selected) object.')) return {'CANCELLED'} if maplus_geom.get_active_object().type != 'MESH': self.report({'ERROR'}, ('Cannot complete: cannot auto-grab source verts ' ' from a non-mesh object.')) return {'CANCELLED'} # Proceed only if selected Blender objects are compatible with the transform target # (Do not allow mesh-level transforms when there are non-mesh objects selected) if not (self.target in { 'MESH_SELECTED', 'WHOLE_MESH', 'OBJECT_ORIGIN' } and [item for item in multi_edit_targets if item.type != 'MESH']): if not hasattr(self, "quick_op_target"): if prims[active_item.axr_axis].kind != 'LINE': self.report( {'ERROR'}, ('Wrong operands: "Axis Rotate" can only operate on ' 'a line')) return {'CANCELLED'} if maplus_geom.get_active_object().type == 'MESH': # a bmesh can only be initialized in edit mode... if previous_mode != 'EDIT': bpy.ops.object.editmode_toggle() else: # else we could already be in edit mode with some stale # updates, exiting and reentering forces an update bpy.ops.object.editmode_toggle() bpy.ops.object.editmode_toggle() # Get global coordinate data for each geometry item, with # modifiers applied. Grab either directly from the scene data # (for quick ops), or from the MAPlus primitives # CollectionProperty on the scene data (for advanced tools) if hasattr(self, 'quick_op_target'): if addon_data.quick_axis_rotate_auto_grab_src: vert_attribs_to_set = ('line_start', 'line_end') try: vert_data = maplus_geom.return_selected_verts( maplus_geom.get_active_object(), len(vert_attribs_to_set), maplus_geom.get_active_object().matrix_world) except maplus_except.InsufficientSelectionError: self.report({'ERROR'}, 'Not enough vertices selected.') return {'CANCELLED'} except maplus_except.NonMeshGrabError: self.report({ 'ERROR' }, 'Cannot grab coords: non-mesh or no active object.') return {'CANCELLED'} maplus_geom.set_item_coords( addon_data.quick_axis_rotate_src, vert_attribs_to_set, vert_data) src_global_data = maplus_geom.get_modified_global_coords( geometry=addon_data.quick_axis_rotate_src, kind='LINE') else: src_global_data = maplus_geom.get_modified_global_coords( geometry=prims[active_item.axr_axis], kind='LINE') # These global point coordinate vectors will be used to construct # geometry and transformations in both object (global) space # and mesh (local) space axis_start = src_global_data[0] axis_end = src_global_data[1] # Get rotation in proper units (radians) if (bpy.context.scene.unit_settings.system_rotation == 'RADIANS'): converted_rot_amount = active_item.axr_amount else: converted_rot_amount = math.radians(active_item.axr_amount) if self.target in {'OBJECT', 'OBJECT_ORIGIN'}: for item in multi_edit_targets: # (Note that there are no transformation modifiers for this # transformation type, so that section is omitted here) # Get the object world matrix before we modify it here item_matrix_unaltered = item.matrix_world.copy() unaltered_inverse = item_matrix_unaltered.copy() unaltered_inverse.invert() # Construct the axis vector and corresponding matrix axis = axis_end - axis_start axis_rot = mathutils.Matrix.Rotation( converted_rot_amount, 4, axis) # Perform the rotation (axis will be realigned later) item.rotation_euler.rotate(axis_rot) bpy.context.view_layer.update() # put the original line starting point (before the object # was rotated) into the local object space src_pivot_location_local = unaltered_inverse @ axis_start # Calculate the new pivot location (after the # first rotation), so that the axis can be moved # back into place new_pivot_loc_global = ( item.matrix_world @ src_pivot_location_local) pivot_to_dest = axis_start - new_pivot_loc_global item.location += pivot_to_dest bpy.context.view_layer.update() if self.target in {'MESH_SELECTED', 'WHOLE_MESH', 'OBJECT_ORIGIN'}: for item in multi_edit_targets: self.report({'WARNING'}, ('Warning/Experimental: mesh transforms' ' on objects with non-uniform scaling' ' are not currently supported.')) # (Note that there are no transformation modifiers for this # transformation type, so that section is omitted here) # Init source mesh src_mesh = bmesh.new() src_mesh.from_mesh(item.data) # Get the object world matrix item_matrix_unaltered_loc = item.matrix_world.copy() unaltered_inverse_loc = item_matrix_unaltered_loc.copy() unaltered_inverse_loc.invert() # Stored geom data in local coords axis_start_loc = unaltered_inverse_loc @ axis_start axis_end_loc = unaltered_inverse_loc @ axis_end # Get axis vector in local space axis_loc = axis_end_loc - axis_start_loc # Get translation, pivot to local origin axis_start_inv = axis_start_loc.copy() axis_start_inv.negate() src_pivot_to_loc_origin = mathutils.Matrix.Translation( axis_start_inv) src_pivot_to_loc_origin.resize_4x4() # Get local axis rotation axis_rot_at_loc_origin = mathutils.Matrix.Rotation( converted_rot_amount, 4, axis_loc) # Get translation, pivot to dest pivot_to_dest = mathutils.Matrix.Translation( axis_start_loc) pivot_to_dest.resize_4x4() axis_rotate_loc = (pivot_to_dest @ axis_rot_at_loc_origin @ src_pivot_to_loc_origin) if self.target == 'MESH_SELECTED': src_mesh.transform(axis_rotate_loc, filter={'SELECT'}) elif self.target == 'WHOLE_MESH': src_mesh.transform(axis_rotate_loc) elif self.target == 'OBJECT_ORIGIN': # Note: a target of 'OBJECT_ORIGIN' is equivalent # to performing an object transf. + an inverse # whole mesh level transf. To the user, # the object appears to stay in the same place, # while only the object's origin moves. src_mesh.transform(axis_rotate_loc.inverted()) bpy.ops.object.mode_set(mode='OBJECT') src_mesh.to_mesh(item.data) src_mesh.free() # Go back to whatever mode we were in before doing this bpy.ops.object.mode_set(mode=previous_mode) else: # The selected Blender objects are not compatible with the # requested transformation type (we can't apply a transform # to mesh data when there are non-mesh objects selected) self.report({'ERROR'}, ('Cannot complete: Cannot apply mesh-level' ' transformations to selected non-mesh objects.')) return {'CANCELLED'} return {'FINISHED'}