Cleanup: strip trailing space, remove BOM
[blender-addons.git] / precision_drawing_tools / pdt_xall.py
blob97aae9c57bcfe19b2f5b97cf53931547749a48a5
1 # ##### BEGIN GPL LICENSE BLOCK #####
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.
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.
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.
17 # ##### END GPL LICENSE BLOCK #####
19 # <pep8 compliant>
21 # ----------------------------------------------------------
22 # Author: Zeffii
23 # Modified by: Alan Odom (Clockmender) & Rune Morling (ermo)
24 # ----------------------------------------------------------
26 import bpy
27 import bmesh
28 from mathutils.geometry import intersect_line_line as LineIntersect
29 import itertools
30 from collections import defaultdict
31 from . import pdt_cad_module as cm
32 from .pdt_functions import oops
33 from .pdt_msg_strings import (
34 PDT_ERR_EDOB_MODE
38 def 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
42 def dist(coord):
43 """Measure distance between two coordinates."""
45 return (v1 - coord).length
47 point_list = sorted(point_list, key=dist)
48 return [v1] + point_list + [v2]
51 def remove_permutations_that_share_a_vertex(bm, permutations):
52 """Get useful Permutations.
54 Args:
55 bm: Object's Bmesh
56 permutations: Possible Intersection Edges as a list
58 Returns:
59 List of Edges.
60 """
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
68 # reaches this point if they do not share.
69 final_permutations.append(edges)
71 return final_permutations
74 def get_valid_permutations(bm, edge_indices):
75 """Get useful Permutations.
77 Args:
78 bm: Object's Bmesh
79 edge_indices: List of indices of Edges to consider
81 Returns:
82 List of suitable Edges.
83 """
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)
90 def 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.
94 Args:
95 closest_points: List of Coordinates of points to consider
96 vert_vectors: List of Coordinates of vertices to consider
98 Returns:
99 Boolean.
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
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
114 def get_intersection_dictionary(bm, edge_indices):
115 """Return a dictionary of edge indices and points found on those edges.
117 Args:
118 bm, Object's Bmesh
119 edge_indices: List of Edge Indices
121 Returns:
122 Dictionary of Vectors.
125 bm.verts.ensure_lookup_table()
126 bm.edges.ensure_lookup_table()
128 permutations = get_valid_permutations(bm, edge_indices)
130 list_k = defaultdict(list)
131 list_d = defaultdict(list)
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)
137 points = LineIntersect(*vert_vectors)
139 # some can be skipped. (NaN, None, not on both edges)
140 if can_skip(points, vert_vectors):
141 continue
143 # reaches this point only when an intersection happens on both edges.
144 [list_k[edge].append(points[0]) for edge in edges]
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)
154 return list_d
157 def update_mesh(bm, int_dict):
158 """Make new geometry (delete old first).
160 Args:
161 bm, Object's Bmesh
162 int_dict: Dictionary of Indices of Vertices
164 Returns:
165 Nothing.
168 orig_e = bm.edges
169 orig_v = bm.verts
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])
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)
186 def unselect_nonintersecting(bm, d_edges, edge_indices):
187 """Deselects Non-Intersection Edges.
189 Args:
190 bm, Object's Bmesh
191 d_edges: List of Intersecting Edges
192 edge_indices: List of Edge Indices to consider
194 Returns:
195 Nothing.
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
204 def intersect_all(context):
205 """Computes All intersections with Crossing Geometry.
207 Note:
208 Deletes original edges and replaces with new intersected edges
210 Args:
211 context: Blender bpy.context instance.
213 Returns:
214 Status Set.
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)
224 if obj.mode == "EDIT":
225 bm = bmesh.from_edit_mesh(obj.data)
227 selected_edges = [edge for edge in bm.edges if edge.select]
228 edge_indices = [i.index for i in selected_edges]
230 int_dict = get_intersection_dictionary(bm, edge_indices)
232 unselect_nonintersecting(bm, int_dict.keys(), edge_indices)
233 update_mesh(bm, int_dict)
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
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
247 class PDT_OT_IntersectAllEdges(bpy.types.Operator):
248 """Cut Selected Edges at All Intersections"""
250 bl_idname = "pdt.intersectall"
251 bl_label = "Intersect All Edges"
252 bl_options = {"REGISTER", "UNDO"}
254 @classmethod
255 def poll(cls, context):
256 """Check to see object is in correct condition.
258 Args:
259 context: Blender bpy.context instance.
261 Returns:
262 Boolean
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"
269 def execute(self, context):
270 """Computes All intersections with Crossing Geometry.
272 Note:
273 Deletes original edges and replaces with new intersected edges
275 Args:
276 context: Blender bpy.context instance.
278 Returns:
279 Status Set.
282 pg = context.scene.pdt_pg
283 pg.command = f"intall"
284 return {"FINISHED"}