1# ##### BEGIN GPL LICENSE BLOCK ##### 2# 3# This program is free software; you can redistribute it and/or 4# modify it under the terms of the GNU General Public License 5# as published by the Free Software Foundation; either version 2 6# of the License, or (at your option) any later version. 7# 8# This program is distributed in the hope that it will be useful, 9# but WITHOUT ANY WARRANTY; without even the implied warranty of 10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11# GNU General Public License for more details. 12# 13# You should have received a copy of the GNU General Public License 14# along with this program; if not, write to the Free Software Foundation, 15# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16# 17# ##### END GPL LICENSE BLOCK ##### 18# 19# <pep8 compliant> 20# 21# ---------------------------------------------------------- 22# Author: Zeffii 23# Modified by: Alan Odom (Clockmender) & Rune Morling (ermo) 24# ---------------------------------------------------------- 25# 26import bpy 27import bmesh 28from mathutils.geometry import intersect_line_line as LineIntersect 29import itertools 30from collections import defaultdict 31from . import pdt_cad_module as cm 32from .pdt_functions import oops 33from .pdt_msg_strings import ( 34 PDT_ERR_EDOB_MODE 35) 36 37 38def order_points(edge, point_list): 39 """Order these edges from distance to v1, then sandwich the sorted list with v1, v2.""" 40 v1, v2 = edge 41 42 def dist(coord): 43 """Measure distance between two coordinates.""" 44 45 return (v1 - coord).length 46 47 point_list = sorted(point_list, key=dist) 48 return [v1] + point_list + [v2] 49 50 51def remove_permutations_that_share_a_vertex(bm, permutations): 52 """Get useful Permutations. 53 54 Args: 55 bm: Object's Bmesh 56 permutations: Possible Intersection Edges as a list 57 58 Returns: 59 List of Edges. 60 """ 61 62 final_permutations = [] 63 for edges in permutations: 64 raw_vert_indices = cm.vertex_indices_from_edges_tuple(bm, edges) 65 if len(set(raw_vert_indices)) < 4: 66 continue 67 68 # reaches this point if they do not share. 69 final_permutations.append(edges) 70 71 return final_permutations 72 73 74def get_valid_permutations(bm, edge_indices): 75 """Get useful Permutations. 76 77 Args: 78 bm: Object's Bmesh 79 edge_indices: List of indices of Edges to consider 80 81 Returns: 82 List of suitable Edges. 83 """ 84 85 raw_permutations = itertools.permutations(edge_indices, 2) 86 permutations = [r for r in raw_permutations if r[0] < r[1]] 87 return remove_permutations_that_share_a_vertex(bm, permutations) 88 89 90def can_skip(closest_points, vert_vectors): 91 """Check if the intersection lies on both edges and return True 92 when criteria are not met, and thus this point can be skipped. 93 94 Args: 95 closest_points: List of Coordinates of points to consider 96 vert_vectors: List of Coordinates of vertices to consider 97 98 Returns: 99 Boolean. 100 """ 101 102 if not closest_points: 103 return True 104 if not isinstance(closest_points[0].x, float): 105 return True 106 if cm.num_edges_point_lies_on(closest_points[0], vert_vectors) < 2: 107 return True 108 109 # if this distance is larger than than 1.0e-5, we can skip it. 110 cpa, cpb = closest_points 111 return (cpa - cpb).length > 1.0e-5 112 113 114def get_intersection_dictionary(bm, edge_indices): 115 """Return a dictionary of edge indices and points found on those edges. 116 117 Args: 118 bm, Object's Bmesh 119 edge_indices: List of Edge Indices 120 121 Returns: 122 Dictionary of Vectors. 123 """ 124 125 bm.verts.ensure_lookup_table() 126 bm.edges.ensure_lookup_table() 127 128 permutations = get_valid_permutations(bm, edge_indices) 129 130 list_k = defaultdict(list) 131 list_d = defaultdict(list) 132 133 for edges in permutations: 134 raw_vert_indices = cm.vertex_indices_from_edges_tuple(bm, edges) 135 vert_vectors = cm.vectors_from_indices(bm, raw_vert_indices) 136 137 points = LineIntersect(*vert_vectors) 138 139 # some can be skipped. (NaN, None, not on both edges) 140 if can_skip(points, vert_vectors): 141 continue 142 143 # reaches this point only when an intersection happens on both edges. 144 [list_k[edge].append(points[0]) for edge in edges] 145 146 # list_k will contain a dict of edge indices and points found on those edges. 147 for edge_idx, unordered_points in list_k.items(): 148 tv1, tv2 = bm.edges[edge_idx].verts 149 v1 = bm.verts[tv1.index].co 150 v2 = bm.verts[tv2.index].co 151 ordered_points = order_points((v1, v2), unordered_points) 152 list_d[edge_idx].extend(ordered_points) 153 154 return list_d 155 156 157def update_mesh(bm, int_dict): 158 """Make new geometry (delete old first). 159 160 Args: 161 bm, Object's Bmesh 162 int_dict: Dictionary of Indices of Vertices 163 164 Returns: 165 Nothing. 166 """ 167 168 orig_e = bm.edges 169 orig_v = bm.verts 170 171 new_verts = [] 172 collect = new_verts.extend 173 for _, point_list in int_dict.items(): 174 num_edges_to_add = len(point_list) - 1 175 for i in range(num_edges_to_add): 176 coord_a = orig_v.new(point_list[i]) 177 coord_b = orig_v.new(point_list[i + 1]) 178 orig_e.new((coord_a, coord_b)) 179 bm.normal_update() 180 collect([coord_a, coord_b]) 181 182 bmesh.ops.delete(bm, geom=[edge for edge in bm.edges if edge.select], context="EDGES") 183 bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001) 184 185 186def unselect_nonintersecting(bm, d_edges, edge_indices): 187 """Deselects Non-Intersection Edges. 188 189 Args: 190 bm, Object's Bmesh 191 d_edges: List of Intersecting Edges 192 edge_indices: List of Edge Indices to consider 193 194 Returns: 195 Nothing. 196 """ 197 198 if len(edge_indices) > len(d_edges): 199 reserved_edges = set(edge_indices) - set(d_edges) 200 for edge in reserved_edges: 201 bm.edges[edge].select = False 202 203 204def intersect_all(context): 205 """Computes All intersections with Crossing Geometry. 206 207 Note: 208 Deletes original edges and replaces with new intersected edges 209 210 Args: 211 context: Blender bpy.context instance. 212 213 Returns: 214 Status Set. 215 """ 216 217 pg = context.scene.pdt_pg 218 obj = context.active_object 219 if all([bool(obj), obj.type == "MESH", obj.mode == "EDIT"]): 220 # must force edge selection mode here 221 bpy.context.tool_settings.mesh_select_mode = (False, True, False) 222 223 224 if obj.mode == "EDIT": 225 bm = bmesh.from_edit_mesh(obj.data) 226 227 selected_edges = [edge for edge in bm.edges if edge.select] 228 edge_indices = [i.index for i in selected_edges] 229 230 int_dict = get_intersection_dictionary(bm, edge_indices) 231 232 unselect_nonintersecting(bm, int_dict.keys(), edge_indices) 233 update_mesh(bm, int_dict) 234 235 bmesh.update_edit_mesh(obj.data) 236 else: 237 pg.error = f"{PDT_ERR_EDOB_MODE},{obj.mode})" 238 context.window_manager.popup_menu(oops, title="Error", icon="ERROR") 239 return 240 241 return 242 else: 243 pg.error = f"{PDT_ERR_EDOB_MODE},{obj.mode})" 244 context.window_manager.popup_menu(oops, title="Error", icon="ERROR") 245 return 246 247class PDT_OT_IntersectAllEdges(bpy.types.Operator): 248 """Cut Selected Edges at All Intersections.""" 249 250 bl_idname = "pdt.intersectall" 251 bl_label = "Intersect All Edges" 252 bl_options = {"REGISTER", "UNDO"} 253 254 @classmethod 255 def poll(cls, context): 256 """Check to see object is in correct condition. 257 258 Args: 259 context: Blender bpy.context instance. 260 261 Returns: 262 Boolean 263 """ 264 obj = context.active_object 265 if obj is None: 266 return False 267 return obj is not None and obj.type == "MESH" and obj.mode == "EDIT" 268 269 def execute(self, context): 270 """Computes All intersections with Crossing Geometry. 271 272 Note: 273 Deletes original edges and replaces with new intersected edges 274 275 Args: 276 context: Blender bpy.context instance. 277 278 Returns: 279 Status Set. 280 """ 281 282 pg = context.scene.pdt_pg 283 pg.command = f"intall" 284 return {"FINISHED"} 285