# SPDX-License-Identifier: GPL-2.0-or-later import bpy import gpu from gpu_extras.batch import batch_for_shader import math import sys import random import bmesh from mathutils import ( Euler, Matrix, Vector, Quaternion, ) from mathutils.geometry import ( intersect_line_plane, ) from math import ( sin, cos, pi, ) import bpy_extras from bpy_extras import view3d_utils from bpy_extras.view3d_utils import ( region_2d_to_vector_3d, region_2d_to_location_3d, location_3d_to_region_2d, ) # Cut Square def CreateCutSquare(self, context): """ Create a rectangle mesh """ far_limit = 10000.0 faces=[] # Get the mouse coordinates coord = self.mouse_path[0][0], self.mouse_path[0][1] # New mesh me = bpy.data.meshes.new('CMT_Square') bm = bmesh.new() bm.from_mesh(me) # New object and link it to the scene ob = bpy.data.objects.new('CMT_Square', me) self.CurrentObj = ob context.collection.objects.link(ob) # Scene information region = context.region rv3d = context.region_data depth_location = region_2d_to_vector_3d(region, rv3d, coord) self.ViewVector = depth_location # Get a point on a infinite plane and its direction plane_normal = depth_location plane_direction = plane_normal.normalized() if self.snapCursor: plane_point = context.scene.cursor.location else: plane_point = self.OpsObj.location if self.OpsObj is not None else Vector((0.0, 0.0, 0.0)) # Find the intersection of a line going thru each vertex and the infinite plane for v_co in self.rectangle_coord: vec = region_2d_to_vector_3d(region, rv3d, v_co) p0 = region_2d_to_location_3d(region, rv3d,v_co, vec) p1 = region_2d_to_location_3d(region, rv3d,v_co, vec) + plane_direction * far_limit faces.append(bm.verts.new(intersect_line_plane(p0, p1, plane_point, plane_direction))) # Update vertices index bm.verts.index_update() # New faces t_face = bm.faces.new(faces) # Set mesh bm.to_mesh(me) # Cut Line def CreateCutLine(self, context): """ Create a polygon mesh """ far_limit = 10000.0 vertices = [] faces = [] loc = [] # Get the mouse coordinates coord = self.mouse_path[0][0], self.mouse_path[0][1] # New mesh me = bpy.data.meshes.new('CMT_Line') bm = bmesh.new() bm.from_mesh(me) # New object and link it to the scene ob = bpy.data.objects.new('CMT_Line', me) self.CurrentObj = ob context.collection.objects.link(ob) # Scene information region = context.region rv3d = context.region_data depth_location = region_2d_to_vector_3d(region, rv3d, coord) self.ViewVector = depth_location # Get a point on a infinite plane and its direction plane_normal = depth_location plane_direction = plane_normal.normalized() if self.snapCursor: plane_point = context.scene.cursor.location else: plane_point = self.OpsObj.location if self.OpsObj is not None else Vector((0.0, 0.0, 0.0)) # Use dict to remove doubles # Find the intersection of a line going thru each vertex and the infinite plane for idx, v_co in enumerate(list(dict.fromkeys(self.mouse_path))): vec = region_2d_to_vector_3d(region, rv3d, v_co) p0 = region_2d_to_location_3d(region, rv3d,v_co, vec) p1 = region_2d_to_location_3d(region, rv3d,v_co, vec) + plane_direction * far_limit loc.append(intersect_line_plane(p0, p1, plane_point, plane_direction)) vertices.append(bm.verts.new(loc[idx])) if idx > 0: bm.edges.new([vertices[idx-1],vertices[idx]]) faces.append(vertices[idx]) # Update vertices index bm.verts.index_update() # Nothing is selected, create close geometry if self.CreateMode: if self.Closed and len(vertices) > 1: bm.edges.new([vertices[-1], vertices[0]]) bm.faces.new(faces) else: # Create faces if more than 2 vertices if len(vertices) > 1 : bm.edges.new([vertices[-1], vertices[0]]) bm.faces.new(faces) bm.to_mesh(me) # Cut Circle def CreateCutCircle(self, context): """ Create a circle mesh """ far_limit = 10000.0 FacesList = [] # Get the mouse coordinates mouse_pos_x = self.mouse_path[0][0] mouse_pos_y = self.mouse_path[0][1] coord = self.mouse_path[0][0], self.mouse_path[0][1] # Scene information region = context.region rv3d = context.region_data depth_location = region_2d_to_vector_3d(region, rv3d, coord) self.ViewVector = depth_location # Get a point on a infinite plane and its direction plane_point = context.scene.cursor.location if self.snapCursor else Vector((0.0, 0.0, 0.0)) plane_normal = depth_location plane_direction = plane_normal.normalized() # New mesh me = bpy.data.meshes.new('CMT_Circle') bm = bmesh.new() bm.from_mesh(me) # New object and link it to the scene ob = bpy.data.objects.new('CMT_Circle', me) self.CurrentObj = ob context.collection.objects.link(ob) # Create a circle using a tri fan tris_fan, indices = draw_circle(self, mouse_pos_x, mouse_pos_y) # Remove the vertex in the center to get the outer line of the circle verts = tris_fan[1:] # Find the intersection of a line going thru each vertex and the infinite plane for vert in verts: vec = region_2d_to_vector_3d(region, rv3d, vert) p0 = region_2d_to_location_3d(region, rv3d, vert, vec) p1 = p0 + plane_direction * far_limit loc0 = intersect_line_plane(p0, p1, plane_point, plane_direction) t_v0 = bm.verts.new(loc0) FacesList.append(t_v0) bm.verts.index_update() bm.faces.new(FacesList) bm.to_mesh(me) def create_2d_circle(self, step, radius, rotation = 0): """ Create the vertices of a 2d circle at (0,0) """ verts = [] for angle in range(0, 360, step): verts.append(math.cos(math.radians(angle + rotation)) * radius) verts.append(math.sin(math.radians(angle + rotation)) * radius) verts.append(0.0) verts.append(math.cos(math.radians(0.0 + rotation)) * radius) verts.append(math.sin(math.radians(0.0 + rotation)) * radius) verts.append(0.0) return(verts) def draw_circle(self, mouse_pos_x, mouse_pos_y): """ Return the coordinates + indices of a circle using a triangle fan """ tris_verts = [] indices = [] segments = int(360 / self.stepAngle[self.step]) radius = self.mouse_path[1][0] - self.mouse_path[0][0] rotation = (self.mouse_path[1][1] - self.mouse_path[0][1]) / 2 # Get the vertices of a 2d circle verts = create_2d_circle(self, self.stepAngle[self.step], radius, rotation) # Create the first vertex at mouse position for the center of the circle tris_verts.append(Vector((mouse_pos_x + self.xpos , mouse_pos_y + self.ypos))) # For each vertex of the circle, add the mouse position and the translation for idx in range(int(len(verts) / 3) - 1): tris_verts.append(Vector((verts[idx * 3] + mouse_pos_x + self.xpos, \ verts[idx * 3 + 1] + mouse_pos_y + self.ypos))) i1 = idx+1 i2 = idx+2 if idx+2 <= segments else 1 indices.append((0,i1,i2)) return(tris_verts, indices) # Object dimensions (SCULPT Tools tips) def objDiagonal(obj): return ((obj.dimensions[0]**2) + (obj.dimensions[1]**2) + (obj.dimensions[2]**2))**0.5 # Bevel Update def update_bevel(context): selection = context.selected_objects.copy() active = context.active_object if len(selection) > 0: for obj in selection: bpy.ops.object.select_all(action='DESELECT') obj.select_set(True) context.view_layer.objects.active = obj # Test object name # Subdive mode : Only bevel weight if obj.data.name.startswith("S_") or obj.data.name.startswith("S "): bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.region_to_loop() bpy.ops.transform.edge_bevelweight(value=1) bpy.ops.object.mode_set(mode='OBJECT') else: # No subdiv mode : bevel weight + Crease + Sharp CreateBevel(context, obj) bpy.ops.object.select_all(action='DESELECT') for obj in selection: obj.select_set(True) context.view_layer.objects.active = active # Create bevel def CreateBevel(context, CurrentObject): # Save active object SavActive = context.active_object # Test if initial object has bevel bevel_modifier = False for modifier in SavActive.modifiers: if modifier.name == 'Bevel': bevel_modifier = True if bevel_modifier: # Active "CurrentObject" context.view_layer.objects.active = CurrentObject bpy.ops.object.mode_set(mode='EDIT') # Edge mode bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='EDGE') # Clear all bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.mark_sharp(clear=True) bpy.ops.transform.edge_crease(value=-1) bpy.ops.transform.edge_bevelweight(value=-1) bpy.ops.mesh.select_all(action='DESELECT') # Select (in radians) all 30° sharp edges bpy.ops.mesh.edges_select_sharp(sharpness=0.523599) # Apply bevel weight + Crease + Sharp to the selected edges bpy.ops.mesh.mark_sharp() bpy.ops.transform.edge_crease(value=1) bpy.ops.transform.edge_bevelweight(value=1) bpy.ops.mesh.select_all(action='DESELECT') bpy.ops.object.mode_set(mode='OBJECT') CurrentObject.data.use_customdata_edge_bevel = True for i in range(len(CurrentObject.data.edges)): if CurrentObject.data.edges[i].select is True: CurrentObject.data.edges[i].bevel_weight = 1.0 CurrentObject.data.edges[i].use_edge_sharp = True bevel_modifier = False for m in CurrentObject.modifiers: if m.name == 'Bevel': bevel_modifier = True if bevel_modifier is False: bpy.ops.object.modifier_add(type='BEVEL') mod = context.object.modifiers[-1] mod.limit_method = 'WEIGHT' mod.width = 0.01 mod.profile = 0.699099 mod.use_clamp_overlap = False mod.segments = 3 mod.loop_slide = False bpy.ops.object.shade_smooth() context.object.data.use_auto_smooth = True context.object.data.auto_smooth_angle = 1.0471975 # Restore the active object context.view_layer.objects.active = SavActive def MoveCursor(qRot, location, self): """ In brush mode : Draw a circle around the brush """ if qRot is not None: verts = create_2d_circle(self, 10, 1) self.CLR_C.clear() vc = Vector() for idx in range(int(len(verts) / 3)): vc.x = verts[idx * 3] vc.y = verts[idx * 3 + 1] vc.z = verts[idx * 3 + 2] vc = qRot @ vc self.CLR_C.append(vc.x) self.CLR_C.append(vc.y) self.CLR_C.append(vc.z) def rot_axis_quat(vector1, vector2): """ Find the rotation (quaternion) from vector 1 to vector 2""" vector1 = vector1.normalized() vector2 = vector2.normalized() cosTheta = vector1.dot(vector2) rotationAxis = Vector((0.0, 0.0, 0.0)) if (cosTheta < -1 + 0.001): v = Vector((0.0, 1.0, 0.0)) #Get the vector at the right angles to both rotationAxis = vector1.cross(v) rotationAxis = rotationAxis.normalized() q = Quaternion() q.w = 0.0 q.x = rotationAxis.x q.y = rotationAxis.y q.z = rotationAxis.z else: rotationAxis = vector1.cross(vector2) s = math.sqrt((1.0 + cosTheta) * 2.0) invs = 1 / s q = Quaternion() q.w = s * 0.5 q.x = rotationAxis.x * invs q.y = rotationAxis.y * invs q.z = rotationAxis.z * invs return q # Picking (template) def Picking(context, event): """ Put the 3d cursor on the closest object""" # get the context arguments scene = context.scene region = context.region rv3d = context.region_data coord = event.mouse_region_x, event.mouse_region_y # get the ray from the viewport and mouse view_vector = view3d_utils.region_2d_to_vector_3d(region, rv3d, coord) ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, coord) ray_target = ray_origin + view_vector def visible_objects_and_duplis(): depsgraph = context.evaluated_depsgraph_get() for dup in depsgraph.object_instances: if dup.is_instance: # Real dupli instance obj = dup.instance_object.original yield (obj, dup.matrix.copy()) else: # Usual object obj = dup.object.original yield (obj, obj.matrix_world.copy()) def obj_ray_cast(obj, matrix): # get the ray relative to the object matrix_inv = matrix.inverted() ray_origin_obj = matrix_inv @ ray_origin ray_target_obj = matrix_inv @ ray_target ray_direction_obj = ray_target_obj - ray_origin_obj # cast the ray success, location, normal, face_index = obj.ray_cast(ray_origin_obj, ray_direction_obj) if success: return location, normal, face_index return None, None, None # cast rays and find the closest object best_length_squared = -1.0 best_obj = None # cast rays and find the closest object for obj, matrix in visible_objects_and_duplis(): if obj.type == 'MESH': hit, normal, face_index = obj_ray_cast(obj, matrix) if hit is not None: hit_world = matrix @ hit length_squared = (hit_world - ray_origin).length_squared if best_obj is None or length_squared < best_length_squared: scene.cursor.location = hit_world best_length_squared = length_squared best_obj = obj else: if best_obj is None: depth_location = region_2d_to_vector_3d(region, rv3d, coord) loc = region_2d_to_location_3d(region, rv3d, coord, depth_location) scene.cursor.location = loc def Pick(context, event, self, ray_max=10000.0): region = context.region rv3d = context.region_data coord = event.mouse_region_x, event.mouse_region_y view_vector = view3d_utils.region_2d_to_vector_3d(region, rv3d, coord) ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, coord) ray_target = ray_origin + (view_vector * ray_max) def obj_ray_cast(obj, matrix): matrix_inv = matrix.inverted() ray_origin_obj = matrix_inv @ ray_origin ray_target_obj = matrix_inv @ ray_target success, hit, normal, face_index = obj.ray_cast(ray_origin_obj, ray_target_obj) if success: return hit, normal, face_index return None, None, None best_length_squared = ray_max * ray_max best_obj = None for obj in self.CList: matrix = obj.matrix_world hit, normal, face_index = obj_ray_cast(obj, matrix) rotation = obj.rotation_euler.to_quaternion() if hit is not None: hit_world = matrix @ hit length_squared = (hit_world - ray_origin).length_squared if length_squared < best_length_squared: best_length_squared = length_squared best_obj = obj hits = hit_world ns = normal fs = face_index if best_obj is not None: return hits, ns, rotation return None, None, None def SelectObject(self, copyobj): copyobj.select_set(True) for child in copyobj.children: SelectObject(self, child) if copyobj.parent is None: bpy.context.view_layer.objects.active = copyobj # Undo def printUndo(self): for l in self.UList: print(l) def UndoAdd(self, type, obj): """ Create a backup mesh before apply the action to the object """ if obj is None: return if type != "DUPLICATE": bm = bmesh.new() bm.from_mesh(obj.data) self.UndoOps.append((obj, type, bm)) else: self.UndoOps.append((obj, type, None)) def UndoListUpdate(self): self.UList.append((self.UndoOps.copy())) self.UList_Index += 1 self.UndoOps.clear() def Undo(self): if self.UList_Index < 0: return # get previous mesh for o in self.UList[self.UList_Index]: if o[1] == "MESH": bm = o[2] bm.to_mesh(o[0].data) SelectObjList = bpy.context.selected_objects.copy() Active_Obj = bpy.context.active_object bpy.ops.object.select_all(action='TOGGLE') for o in self.UList[self.UList_Index]: if o[1] == "REBOOL": o[0].select_set(True) o[0].hide_viewport = False if o[1] == "DUPLICATE": o[0].select_set(True) o[0].hide_viewport = False bpy.ops.object.delete(use_global=False) for so in SelectObjList: bpy.data.objects[so.name].select_set(True) bpy.context.view_layer.objects.active = Active_Obj self.UList_Index -= 1 self.UList[self.UList_Index + 1:] = [] def duplicateObject(self): if self.Instantiate: bpy.ops.object.duplicate_move_linked( OBJECT_OT_duplicate={ "linked": True, "mode": 'TRANSLATION', }, TRANSFORM_OT_translate={ "value": (0, 0, 0), }, ) else: bpy.ops.object.duplicate_move( OBJECT_OT_duplicate={ "linked": False, "mode": 'TRANSLATION', }, TRANSFORM_OT_translate={ "value": (0, 0, 0), }, ) ob_new = bpy.context.active_object ob_new.location = self.CurLoc v = Vector() v.x = v.y = 0.0 v.z = self.BrushDepthOffset ob_new.location += self.qRot * v if self.ObjectMode: ob_new.scale = self.ObjectBrush.scale if self.ProfileMode: ob_new.scale = self.ProfileBrush.scale e = Euler() e.x = e.y = 0.0 e.z = self.aRotZ / 25.0 # If duplicate with a grid, no random rotation (each mesh in the grid is already rotated randomly) if (self.alt is True) and ((self.nbcol + self.nbrow) < 3): if self.RandomRotation: e.z += random.random() qe = e.to_quaternion() qRot = self.qRot * qe ob_new.rotation_mode = 'QUATERNION' ob_new.rotation_quaternion = qRot ob_new.rotation_mode = 'XYZ' if (ob_new.display_type == "WIRE") and (self.BrushSolidify is False): ob_new.hide_viewport = True if self.BrushSolidify: ob_new.display_type = "SOLID" ob_new.show_in_front = False for o in bpy.context.selected_objects: UndoAdd(self, "DUPLICATE", o) if len(bpy.context.selected_objects) > 0: bpy.ops.object.select_all(action='TOGGLE') for o in self.all_sel_obj_list: o.select_set(True) bpy.context.view_layer.objects.active = self.OpsObj def update_grid(self, context): """ Thanks to batFINGER for his help : source : http://blender.stackexchange.com/questions/55864/multiple-meshes-not-welded-with-pydata """ verts = [] edges = [] faces = [] numface = 0 if self.nbcol < 1: self.nbcol = 1 if self.nbrow < 1: self.nbrow = 1 if self.gapx < 0: self.gapx = 0 if self.gapy < 0: self.gapy = 0 # Get the data from the profils or the object if self.ProfileMode: brush = bpy.data.objects.new( self.Profils[self.nProfil][0], bpy.data.meshes[self.Profils[self.nProfil][0]] ) obj = bpy.data.objects["CT_Profil"] obfaces = brush.data.polygons obverts = brush.data.vertices lenverts = len(obverts) else: brush = bpy.data.objects["CarverBrushCopy"] obj = context.selected_objects[0] obverts = brush.data.vertices obfaces = brush.data.polygons lenverts = len(brush.data.vertices) # Gap between each row / column gapx = self.gapx gapy = self.gapy # Width of each row / column widthx = brush.dimensions.x * self.scale_x widthy = brush.dimensions.y * self.scale_y # Compute the corners so the new object will be always at the center left = -((self.nbcol - 1) * (widthx + gapx)) / 2 start = -((self.nbrow - 1) * (widthy + gapy)) / 2 for i in range(self.nbrow * self.nbcol): row = i % self.nbrow col = i // self.nbrow startx = left + ((widthx + gapx) * col) starty = start + ((widthy + gapy) * row) # Add random rotation if (self.RandomRotation) and not (self.GridScaleX or self.GridScaleY): rotmat = Matrix.Rotation(math.radians(360 * random.random()), 4, 'Z') for v in obverts: v.co = v.co @ rotmat verts.extend([((v.co.x - startx, v.co.y - starty, v.co.z)) for v in obverts]) faces.extend([[v + numface * lenverts for v in p.vertices] for p in obfaces]) numface += 1 # Update the mesh # Create mesh data mymesh = bpy.data.meshes.new("CT_Profil") # Generate mesh data mymesh.from_pydata(verts, edges, faces) # Calculate the edges mymesh.update(calc_edges=True) # Update data obj.data = mymesh # Make the object active to remove doubles context.view_layer.objects.active = obj def boolean_operation(bool_type="DIFFERENCE"): ActiveObj = bpy.context.active_object sel_index = 0 if bpy.context.selected_objects[0] != bpy.context.active_object else 1 # bpy.ops.object.modifier_apply(modifier="CT_SOLIDIFY") bool_name = "CT_" + bpy.context.selected_objects[sel_index].name BoolMod = ActiveObj.modifiers.new(bool_name, "BOOLEAN") BoolMod.object = bpy.context.selected_objects[sel_index] BoolMod.operation = bool_type bpy.context.selected_objects[sel_index].display_type = 'WIRE' while ActiveObj.modifiers.find(bool_name) > 0: bpy.ops.object.modifier_move_up(modifier=bool_name) def Rebool(context, self): target_obj = context.active_object Brush = context.selected_objects[1] Brush.display_type = "WIRE" #Deselect all bpy.ops.object.select_all(action='TOGGLE') target_obj.display_type = "SOLID" target_obj.select_set(True) bpy.ops.object.duplicate() rebool_obj = context.active_object m = rebool_obj.modifiers.new("CT_INTERSECT", "BOOLEAN") m.operation = "INTERSECT" m.object = Brush m = target_obj.modifiers.new("CT_DIFFERENCE", "BOOLEAN") m.operation = "DIFFERENCE" m.object = Brush for mb in target_obj.modifiers: if mb.type == 'BEVEL': mb.show_viewport = False if self.ObjectBrush or self.ProfileBrush: rebool_obj.show_in_front = False try: bpy.ops.object.modifier_apply(modifier="CT_SOLIDIFY") except: exc_type, exc_value, exc_traceback = sys.exc_info() self.report({'ERROR'}, str(exc_value)) if self.dont_apply_boolean is False: try: bpy.ops.object.modifier_apply(modifier="CT_INTERSECT") except: exc_type, exc_value, exc_traceback = sys.exc_info() self.report({'ERROR'}, str(exc_value)) bpy.ops.object.select_all(action='TOGGLE') for mb in target_obj.modifiers: if mb.type == 'BEVEL': mb.show_viewport = True context.view_layer.objects.active = target_obj target_obj.select_set(True) if self.dont_apply_boolean is False: try: bpy.ops.object.modifier_apply(modifier="CT_DIFFERENCE") except: exc_type, exc_value, exc_traceback = sys.exc_info() self.report({'ERROR'}, str(exc_value)) bpy.ops.object.select_all(action='TOGGLE') rebool_obj.select_set(True) def createMeshFromData(self): if self.Profils[self.nProfil][0] not in bpy.data.meshes: # Create mesh and object me = bpy.data.meshes.new(self.Profils[self.nProfil][0]) # Create mesh from given verts, faces. me.from_pydata(self.Profils[self.nProfil][2], [], self.Profils[self.nProfil][3]) me.validate(verbose=True, clean_customdata=True) # Update mesh with new data me.update() if "CT_Profil" not in bpy.data.objects: ob = bpy.data.objects.new("CT_Profil", bpy.data.meshes[self.Profils[self.nProfil][0]]) ob.location = Vector((0.0, 0.0, 0.0)) # Link object to scene and make active bpy.context.collection.objects.link(ob) bpy.context.view_layer.update() bpy.context.view_layer.objects.active = ob ob.select_set(True) ob.location = Vector((10000.0, 0.0, 0.0)) ob.display_type = "WIRE" self.SolidifyPossible = True else: bpy.data.objects["CT_Profil"].data = bpy.data.meshes[self.Profils[self.nProfil][0]] def Selection_Save_Restore(self): if "CT_Profil" in bpy.data.objects: Selection_Save(self) bpy.ops.object.select_all(action='DESELECT') bpy.data.objects["CT_Profil"].select_set(True) bpy.context.view_layer.objects.active = bpy.data.objects["CT_Profil"] if bpy.data.objects["CT_Profil"] in self.all_sel_obj_list: self.all_sel_obj_list.remove(bpy.data.objects["CT_Profil"]) bpy.ops.object.delete(use_global=False) Selection_Restore(self) def Selection_Save(self): obj_name = getattr(bpy.context.active_object, "name", None) self.all_sel_obj_list = bpy.context.selected_objects.copy() self.save_active_obj = obj_name def Selection_Restore(self): for o in self.all_sel_obj_list: o.select_set(True) if self.save_active_obj: bpy.context.view_layer.objects.active = bpy.data.objects.get(self.save_active_obj, None) def Snap_Cursor(self, context, event, mouse_pos): """ Find the closest position on the overlay grid and snap the mouse on it """ # Get the context arguments region = context.region rv3d = context.region_data # Get the VIEW3D area for i, a in enumerate(context.screen.areas): if a.type == 'VIEW_3D': space = context.screen.areas[i].spaces.active # Get the grid overlay for the VIEW_3D grid_scale = space.overlay.grid_scale grid_subdivisions = space.overlay.grid_subdivisions # Use the grid scale and subdivision to get the increment increment = (grid_scale / grid_subdivisions) half_increment = increment / 2 # Convert the 2d location of the mouse in 3d for index, loc in enumerate(reversed(mouse_pos)): mouse_loc_3d = region_2d_to_location_3d(region, rv3d, loc, (0, 0, 0)) # Get the remainder from the mouse location and the ratio # Test if the remainder > to the half of the increment for i in range(3): modulo = mouse_loc_3d[i] % increment if modulo < half_increment: modulo = - modulo else: modulo = increment - modulo # Add the remainder to get the closest location on the grid mouse_loc_3d[i] = mouse_loc_3d[i] + modulo # Get the snapped 2d location snap_loc_2d = location_3d_to_region_2d(region, rv3d, mouse_loc_3d) # Replace the last mouse location by the snapped location if len(self.mouse_path) > 0: self.mouse_path[len(self.mouse_path) - (index + 1) ] = tuple(snap_loc_2d) def mini_grid(self, context, color): """ Draw a snap mini grid around the cursor based on the overlay grid""" # Get the context arguments region = context.region rv3d = context.region_data # Get the VIEW3D area for i, a in enumerate(context.screen.areas): if a.type == 'VIEW_3D': space = context.screen.areas[i].spaces.active screen_height = context.screen.areas[i].height screen_width = context.screen.areas[i].width #Draw the snap grid, only in ortho view if not space.region_3d.is_perspective : grid_scale = space.overlay.grid_scale grid_subdivisions = space.overlay.grid_subdivisions increment = (grid_scale / grid_subdivisions) # Get the 3d location of the mouse forced to a snap value in the operator mouse_coord = self.mouse_path[len(self.mouse_path) - 1] snap_loc = region_2d_to_location_3d(region, rv3d, mouse_coord, (0, 0, 0)) # Add the increment to get the closest location on the grid snap_loc[0] += increment snap_loc[1] += increment # Get the 2d location of the snap location snap_loc = location_3d_to_region_2d(region, rv3d, snap_loc) origin = location_3d_to_region_2d(region, rv3d, (0,0,0)) # Get the increment value snap_value = snap_loc[0] - mouse_coord[0] grid_coords = [] # Draw lines on X and Z axis from the cursor through the screen grid_coords = [ (0, mouse_coord[1]), (screen_width, mouse_coord[1]), (mouse_coord[0], 0), (mouse_coord[0], screen_height) ] # Draw a mlini grid around the cursor to show the snap options grid_coords += [ (mouse_coord[0] + snap_value, mouse_coord[1] + 25 + snap_value), (mouse_coord[0] + snap_value, mouse_coord[1] - 25 - snap_value), (mouse_coord[0] + 25 + snap_value, mouse_coord[1] + snap_value), (mouse_coord[0] - 25 - snap_value, mouse_coord[1] + snap_value), (mouse_coord[0] - snap_value, mouse_coord[1] + 25 + snap_value), (mouse_coord[0] - snap_value, mouse_coord[1] - 25 - snap_value), (mouse_coord[0] + 25 + snap_value, mouse_coord[1] - snap_value), (mouse_coord[0] - 25 - snap_value, mouse_coord[1] - snap_value), ] draw_shader(self, color, 0.3, 'LINES', grid_coords, size=2) def draw_shader(self, color, alpha, type, coords, size=1, indices=None): """ Create a batch for a draw type """ gpu.state.blend_set('ALPHA') if type =='POINTS': gpu.state.program_point_size_set(False) gpu.state.point_size_set(size) shader = gpu.shader.from_builtin('UNIFORM_COLOR') else: shader = gpu.shader.from_builtin('POLYLINE_UNIFORM_COLOR') shader.uniform_float("viewportSize", gpu.state.viewport_get()[2:]) shader.uniform_float("lineWidth", 1.0) try: shader.uniform_float("color", (color[0], color[1], color[2], alpha)) batch = batch_for_shader(shader, type, {"pos": coords}, indices=indices) batch.draw(shader) except: exc_type, exc_value, exc_traceback = sys.exc_info() self.report({'ERROR'}, str(exc_value)) gpu.state.point_size_set(1.0) gpu.state.blend_set('NONE')