### BEGIN GPL LICENSE BLOCK ##### # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 3 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # ##### END GPL LICENSE BLOCK ##### import bpy, bmesh from bpy.props import FloatProperty from mathutils import Vector from mathutils.geometry import intersect_point_line from .common_classes import ( SnapDrawn, CharMap, SnapNavigation, g_snap_widget, #TODO: remove ) from .common_utilities import ( get_units_info, convert_distance, snap_utilities, ) if not __package__: __package__ = "mesh_snap_utilities" def get_closest_edge(bm, point, dist): r_edge = None for edge in bm.edges: v1 = edge.verts[0].co v2 = edge.verts[1].co # Test the BVH (AABB) first for i in range(3): if v1[i] <= v2[i]: isect = v1[i] - dist <= point[i] <= v2[i] + dist else: isect = v2[i] - dist <= point[i] <= v1[i] + dist if not isect: break else: ret = intersect_point_line(point, v1, v2) if ret[1] < 0.0: tmp = v1 elif ret[1] > 1.0: tmp = v2 else: tmp = ret[0] new_dist = (point - tmp).length if new_dist <= dist: dist = new_dist r_edge = edge return r_edge def get_loose_linked_edges(bmvert): linked = [e for e in bmvert.link_edges if not e.link_faces] for e in linked: linked += [le for v in e.verts if not v.link_faces for le in v.link_edges if le not in linked] return linked def draw_line(self, bm_geom, location): obj = self.main_snap_obj.data[0] bm = self.main_bm split_faces = set() update_edit_mesh = False if bm_geom is None: vert = bm.verts.new(location) self.list_verts.append(vert) update_edit_mesh = True elif isinstance(bm_geom, bmesh.types.BMVert): if (bm_geom.co - location).length_squared < .001: if self.list_verts == [] or self.list_verts[-1] != bm_geom: self.list_verts.append(bm_geom) else: vert = bm.verts.new(location) self.list_verts.append(vert) update_edit_mesh = True elif isinstance(bm_geom, bmesh.types.BMEdge): self.list_edges.append(bm_geom) ret = intersect_point_line(location, bm_geom.verts[0].co, bm_geom.verts[1].co) if (ret[0] - location).length_squared < .001: if ret[1] == 0.0: vert = bm_geom.verts[0] elif ret[1] == 1.0: vert = bm_geom.verts[1] else: edge, vert = bmesh.utils.edge_split(bm_geom, bm_geom.verts[0], ret[1]) update_edit_mesh = True self.list_verts.append(vert) self.geom = vert # hack to highlight in the drawing # self.list_edges.append(edge) else: # constrain point is near vert = bm.verts.new(location) self.list_verts.append(vert) update_edit_mesh = True elif isinstance(bm_geom, bmesh.types.BMFace): split_faces.add(bm_geom) vert = bm.verts.new(location) self.list_verts.append(vert) update_edit_mesh = True # draw, split and create face if len(self.list_verts) >= 2: v1, v2 = self.list_verts[-2:] edge = bm.edges.get([v1, v2]) if edge: self.list_edges.append(edge) else: if not v2.link_edges: edge = bm.edges.new([v1, v2]) self.list_edges.append(edge) else: # split face v1_link_faces = v1.link_faces v2_link_faces = v2.link_faces if v1_link_faces and v2_link_faces: split_faces.update(set(v1_link_faces).intersection(v2_link_faces)) else: if v1_link_faces: faces = v1_link_faces co2 = v2.co.copy() else: faces = v2_link_faces co2 = v1.co.copy() for face in faces: if bmesh.geometry.intersect_face_point(face, co2): co = co2 - face.calc_center_median() if co.dot(face.normal) < 0.001: split_faces.add(face) if split_faces: edge = bm.edges.new([v1, v2]) self.list_edges.append(edge) ed_list = get_loose_linked_edges(v2) for face in split_faces: facesp = bmesh.utils.face_split_edgenet(face, ed_list) del split_faces else: if self.intersect: facesp = bmesh.ops.connect_vert_pair(bm, verts=[v1, v2], verts_exclude=bm.verts) # print(facesp) if not self.intersect or not facesp['edges']: edge = bm.edges.new([v1, v2]) self.list_edges.append(edge) else: for edge in facesp['edges']: self.list_edges.append(edge) update_edit_mesh = True # create face if self.create_face: ed_list = set(self.list_edges) for edge in v2.link_edges: if edge not in ed_list and edge.other_vert(v2) in self.list_verts: ed_list.add(edge) break ed_list.update(get_loose_linked_edges(v2)) bmesh.ops.edgenet_fill(bm, edges=list(ed_list)) update_edit_mesh = True # print('face created') if update_edit_mesh: obj.data.update_gpu_tag() obj.data.update_tag() obj.update_from_editmode() obj.update_tag() bmesh.update_edit_mesh(obj.data) self.sctx.tag_update_drawn_snap_object(self.main_snap_obj) #bm.verts.index_update() bpy.ops.ed.undo_push(message="Undo draw line*") return [obj.matrix_world @ v.co for v in self.list_verts] class SnapUtilitiesLine(bpy.types.Operator): """Make Lines. Connect them to split faces""" bl_idname = "mesh.make_line" bl_label = "Line Tool" bl_options = {'REGISTER'} wait_for_input: bpy.props.BoolProperty(name="Wait for Input", default=True) constrain_keys = { 'X': Vector((1,0,0)), 'Y': Vector((0,1,0)), 'Z': Vector((0,0,1)), 'RIGHT_SHIFT': 'shift', 'LEFT_SHIFT': 'shift', } def _exit(self, context): del self.main_bm #avoids unpredictable crashs del self.bm del self.list_edges del self.list_verts del self.list_verts_co bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') context.area.header_text_set(None) if not self.snap_widget: self.sctx.free() del self.draw_cache else: self.sctx = None self.draw_cache = None #Restore initial state context.tool_settings.mesh_select_mode = self.select_mode context.space_data.overlay.show_face_center = self.show_face_center def _init_snap_context(self, context): if self.sctx: self.sctx.clear_snap_objects() else: #Create Snap Context from .snap_context_l import SnapContext self.sctx = SnapContext(context.region, context.space_data) self.sctx.set_pixel_dist(12) self.sctx.use_clip_planes(True) if self.outer_verts: for base in context.visible_bases: self.sctx.add_obj(base.object, base.object.matrix_world) self.sctx.set_snap_mode(True, True, self.snap_face) if self.snap_widget: self.geom = self.snap_widget.geom self.type = self.snap_widget.type self.location = self.snap_widget.loc if self.snap_widget.snap_obj: context.view_layer.objects.active = self.snap_widget.snap_obj.data[0] else: #init these variables to avoid errors self.geom = None self.type = 'OUT' self.location = Vector() self.prevloc = Vector() self.list_verts = [] self.list_edges = [] self.list_verts_co = [] self.bool_update = False self.vector_constrain = () self.len = 0 self.length_entered = "" self.length_entered_value = 0.0 self.line_pos = 0 active_object = context.active_object mesh = active_object.data self.main_snap_obj = self.snap_obj = self.sctx._get_snap_obj_by_obj(active_object) self.main_bm = self.bm = bmesh.from_edit_mesh(mesh) def modal(self, context, event): if self.navigation_ops.run(context, event, self.prevloc if self.vector_constrain else self.location): return {'RUNNING_MODAL'} context.area.tag_redraw() if event.ctrl and event.type == 'Z' and event.value == 'PRESS': del self.bm del self.main_bm bpy.ops.ed.undo() bpy.ops.object.mode_set(mode='EDIT') # just to be sure bpy.ops.mesh.select_all(action='DESELECT') context.tool_settings.mesh_select_mode = (True, False, True) context.space_data.overlay.show_face_center = True self._init_snap_context(context) self.sctx.update_all() return {'RUNNING_MODAL'} is_making_lines = bool(self.list_verts_co) if (event.type == 'MOUSEMOVE' or self.bool_update) and self.length_entered_value == 0.0: if self.rv3d.view_matrix != self.rotMat: self.rotMat = self.rv3d.view_matrix.copy() self.bool_update = True snap_utilities.cache.snp_obj = None # hack for snap edge elemens update else: self.bool_update = False mval = Vector((event.mouse_region_x, event.mouse_region_y)) self.snap_obj, self.prevloc, self.location, self.type, self.bm, self.geom, self.len = snap_utilities( self.sctx, self.main_snap_obj, mval, constrain=self.vector_constrain, previous_vert=(self.list_verts[-1] if self.list_verts else None), increment=self.incremental ) if self.snap_to_grid and self.type == 'OUT': loc = self.location / self.rd self.location = Vector((round(loc.x), round(loc.y), round(loc.z))) * self.rd if is_making_lines and self.keyf8: lloc = self.list_verts_co[-1] view_vec, orig = self.sctx.last_ray location = intersect_point_line(lloc, orig, (orig + view_vec)) vec = (location[0] - lloc) ax, ay, az = abs(vec.x), abs(vec.y), abs(vec.z) vec.x = ax > ay and ax > az vec.y = ay > ax and ay > az vec.z = az > ay and az > ax if not (vec.x or vec.y or vec.z): self.vector_constrain = None else: vc = lloc + vec if self.vector_constrain is None or vc != self.vector_constrain[1]: type = 'X' if vec.x else 'Y' if vec.y else 'Z' if vec.z else 'shift' self.vector_constrain = [lloc, vc, type] #del vc, type #del view_vec, orig, location, ax, ay, az, vec if event.value == 'PRESS': if is_making_lines and (event.ascii in CharMap.ascii or event.type in CharMap.type): CharMap.modal(self, context, event) if self.length_entered != "": try: self.length_entered_value = bpy.utils.units.to_value( self.unit_system, 'LENGTH', self.length_entered) vector = (self.location - self.list_verts_co[-1]).normalized() self.location = (self.list_verts_co[-1] + (vector * self.length_entered_value)) del vector except: # ValueError: self.length_entered_value = 0.0 #invalid self.report({'INFO'}, "Operation not supported yet") else: self.length_entered_value = 0.0 self.bool_update = True elif event.type in self.constrain_keys: self.bool_update = True self.keyf8 = False if self.vector_constrain and self.vector_constrain[2] == event.type: self.vector_constrain = () else: if event.shift: if isinstance(self.geom, bmesh.types.BMEdge): if is_making_lines: loc = self.list_verts_co[-1] self.vector_constrain = (loc, loc + self.geom.verts[1].co - self.geom.verts[0].co, event.type) else: self.vector_constrain = [self.main_snap_obj.mat @ v.co for v in self.geom.verts] + [event.type] else: if is_making_lines: loc = self.list_verts_co[-1] else: loc = self.location self.vector_constrain = [loc, loc + self.constrain_keys[event.type], event.type] elif event.type in {'LEFTMOUSE', 'RET', 'NUMPAD_ENTER'}: if event.type == 'LEFTMOUSE' or self.length_entered_value != 0.0: if not is_making_lines and self.bm: self.main_snap_obj = self.snap_obj self.main_bm = self.bm mat_inv = self.main_snap_obj.mat.inverted_safe() point = mat_inv @ self.location # with constraint the intersection can be in a different element of the selected one geom2 = self.geom if geom2: geom2.select = False if self.vector_constrain: geom2 = get_closest_edge(self.main_bm, point, .001) self.list_verts_co = draw_line(self, geom2, point) self.vector_constrain = None self.length_entered = "" self.length_entered_value = 0.0 self.line_pos = 0 else: self._exit(context) return {'FINISHED'} elif event.type == 'F8': self.vector_constrain = None self.keyf8 = self.keyf8 is False elif event.type in {'RIGHTMOUSE', 'ESC'}: if not self.wait_for_input or not is_making_lines or event.type == 'ESC': if self.geom: self.geom.select = True self._exit(context) return {'FINISHED'} else: snap_utilities.cache.snp_obj = None # hack for snap edge elemens update self.vector_constrain = None self.list_edges = [] self.list_verts = [] self.list_verts_co = [] self.length_entered = "" self.length_entered_value = 0.0 self.line_pos = 0 a = "" if is_making_lines: if self.length_entered: pos = self.line_pos a = 'length: ' + self.length_entered[:pos] + '|' + self.length_entered[pos:] else: length = self.len length = convert_distance(length, self.uinfo) a = 'length: ' + length context.area.header_text_set(text = "hit: %.3f %.3f %.3f %s" % (*self.location, a)) if True or is_making_lines: return {'RUNNING_MODAL'} return {'PASS_THROUGH'} def draw_callback_px(self): if self.bm: self.draw_cache.draw_elem(self.snap_obj, self.bm, self.geom) self.draw_cache.draw(self.type, self.location, self.list_verts_co, self.vector_constrain, self.prevloc) def invoke(self, context, event): if context.space_data.type == 'VIEW_3D': # print('name', __name__, __package__) #Store current state self.select_mode = context.tool_settings.mesh_select_mode[:] self.show_face_center = context.space_data.overlay.show_face_center #Modify the current state bpy.ops.mesh.select_all(action='DESELECT') context.tool_settings.mesh_select_mode = (True, False, True) context.space_data.overlay.show_face_center = True #Store values from 3d view context self.rv3d = context.region_data self.rotMat = self.rv3d.view_matrix.copy() # self.obj_glmatrix = bgl.Buffer(bgl.GL_FLOAT, [4, 4], self.obj_matrix.transposed()) #Init event variables self.keyf8 = False self.snap_face = True self.snap_widget = g_snap_widget[0] if self.snap_widget is not None: self.draw_cache = self.snap_widget.draw_cache self.sctx = self.snap_widget.sctx preferences = self.snap_widget.preferences else: preferences = context.preferences.addons[__package__].preferences #Init DrawCache self.draw_cache = SnapDrawn( preferences.out_color, preferences.face_color, preferences.edge_color, preferences.vert_color, preferences.center_color, preferences.perpendicular_color, preferences.constrain_shift_color, tuple(context.preferences.themes[0].user_interface.axis_x) + (1.0,), tuple(context.preferences.themes[0].user_interface.axis_y) + (1.0,), tuple(context.preferences.themes[0].user_interface.axis_z) + (1.0,) ) self.sctx = None #Configure the unit of measure self.unit_system = context.scene.unit_settings.system scale = context.scene.unit_settings.scale_length separate_units = context.scene.unit_settings.use_separate self.uinfo = get_units_info(scale, self.unit_system, separate_units) scale /= context.space_data.overlay.grid_scale * preferences.relative_scale self.rd = bpy.utils.units.to_value(self.unit_system, 'LENGTH', str(1 / scale)) self.intersect = preferences.intersect self.create_face = preferences.create_face self.outer_verts = preferences.outer_verts self.snap_to_grid = preferences.increments_grid self.incremental = bpy.utils.units.to_value(self.unit_system, 'LENGTH', str(preferences.incremental)) self.navigation_ops = SnapNavigation(context, True) self._init_snap_context(context) #modals context.window_manager.modal_handler_add(self) if not self.wait_for_input: self.modal(context, event) self._handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_callback_px, (), 'WINDOW', 'POST_VIEW') return {'RUNNING_MODAL'} else: self.report({'WARNING'}, "Active space must be a View3d") return {'CANCELLED'} def register(): bpy.utils.register_class(SnapUtilitiesLine) if __name__ == "__main__": register()