AnimAll: rename the "Animate" tab back to "Animation"
[blender-addons.git] / precision_drawing_tools / pdt_xall.py
blob3cf82c041c34ab3ef48b382ddcc3d28782d5c571
1 # SPDX-FileCopyrightText: 2016-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
6 # ----------------------------------------------------------
7 # Author: Zeffii
8 # Modified by: Alan Odom (Clockmender) & Rune Morling (ermo)
9 # ----------------------------------------------------------
11 import bpy
12 import bmesh
13 from mathutils.geometry import intersect_line_line as LineIntersect
14 import itertools
15 from collections import defaultdict
16 from . import pdt_cad_module as cm
17 from .pdt_functions import oops
18 from .pdt_msg_strings import (
19 PDT_ERR_EDOB_MODE
23 def order_points(edge, point_list):
24 """Order these edges from distance to v1, then sandwich the sorted list with v1, v2."""
25 v1, v2 = edge
27 def dist(coord):
28 """Measure distance between two coordinates."""
30 return (v1 - coord).length
32 point_list = sorted(point_list, key=dist)
33 return [v1] + point_list + [v2]
36 def remove_permutations_that_share_a_vertex(bm, permutations):
37 """Get useful Permutations.
39 Args:
40 bm: Object's Bmesh
41 permutations: Possible Intersection Edges as a list
43 Returns:
44 List of Edges.
45 """
47 final_permutations = []
48 for edges in permutations:
49 raw_vert_indices = cm.vertex_indices_from_edges_tuple(bm, edges)
50 if len(set(raw_vert_indices)) < 4:
51 continue
53 # reaches this point if they do not share.
54 final_permutations.append(edges)
56 return final_permutations
59 def get_valid_permutations(bm, edge_indices):
60 """Get useful Permutations.
62 Args:
63 bm: Object's Bmesh
64 edge_indices: List of indices of Edges to consider
66 Returns:
67 List of suitable Edges.
68 """
70 raw_permutations = itertools.permutations(edge_indices, 2)
71 permutations = [r for r in raw_permutations if r[0] < r[1]]
72 return remove_permutations_that_share_a_vertex(bm, permutations)
75 def can_skip(closest_points, vert_vectors):
76 """Check if the intersection lies on both edges and return True
77 when criteria are not met, and thus this point can be skipped.
79 Args:
80 closest_points: List of Coordinates of points to consider
81 vert_vectors: List of Coordinates of vertices to consider
83 Returns:
84 Boolean.
85 """
87 if not closest_points:
88 return True
89 if not isinstance(closest_points[0].x, float):
90 return True
91 if cm.num_edges_point_lies_on(closest_points[0], vert_vectors) < 2:
92 return True
94 # if this distance is larger than than 1.0e-5, we can skip it.
95 cpa, cpb = closest_points
96 return (cpa - cpb).length > 1.0e-5
99 def get_intersection_dictionary(bm, edge_indices):
100 """Return a dictionary of edge indices and points found on those edges.
102 Args:
103 bm, Object's Bmesh
104 edge_indices: List of Edge Indices
106 Returns:
107 Dictionary of Vectors.
110 bm.verts.ensure_lookup_table()
111 bm.edges.ensure_lookup_table()
113 permutations = get_valid_permutations(bm, edge_indices)
115 list_k = defaultdict(list)
116 list_d = defaultdict(list)
118 for edges in permutations:
119 raw_vert_indices = cm.vertex_indices_from_edges_tuple(bm, edges)
120 vert_vectors = cm.vectors_from_indices(bm, raw_vert_indices)
122 points = LineIntersect(*vert_vectors)
124 # some can be skipped. (NaN, None, not on both edges)
125 if can_skip(points, vert_vectors):
126 continue
128 # reaches this point only when an intersection happens on both edges.
129 [list_k[edge].append(points[0]) for edge in edges]
131 # list_k will contain a dict of edge indices and points found on those edges.
132 for edge_idx, unordered_points in list_k.items():
133 tv1, tv2 = bm.edges[edge_idx].verts
134 v1 = bm.verts[tv1.index].co
135 v2 = bm.verts[tv2.index].co
136 ordered_points = order_points((v1, v2), unordered_points)
137 list_d[edge_idx].extend(ordered_points)
139 return list_d
142 def update_mesh(bm, int_dict):
143 """Make new geometry (delete old first).
145 Args:
146 bm, Object's Bmesh
147 int_dict: Dictionary of Indices of Vertices
149 Returns:
150 Nothing.
153 orig_e = bm.edges
154 orig_v = bm.verts
156 new_verts = []
157 collect = new_verts.extend
158 for _, point_list in int_dict.items():
159 num_edges_to_add = len(point_list) - 1
160 for i in range(num_edges_to_add):
161 coord_a = orig_v.new(point_list[i])
162 coord_b = orig_v.new(point_list[i + 1])
163 orig_e.new((coord_a, coord_b))
164 bm.normal_update()
165 collect([coord_a, coord_b])
167 bmesh.ops.delete(bm, geom=[edge for edge in bm.edges if edge.select], context="EDGES")
168 bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
171 def unselect_nonintersecting(bm, d_edges, edge_indices):
172 """Deselects Non-Intersection Edges.
174 Args:
175 bm, Object's Bmesh
176 d_edges: List of Intersecting Edges
177 edge_indices: List of Edge Indices to consider
179 Returns:
180 Nothing.
183 if len(edge_indices) > len(d_edges):
184 reserved_edges = set(edge_indices) - set(d_edges)
185 for edge in reserved_edges:
186 bm.edges[edge].select = False
189 def intersect_all(context):
190 """Computes All intersections with Crossing Geometry.
192 Note:
193 Deletes original edges and replaces with new intersected edges
195 Args:
196 context: Blender bpy.context instance.
198 Returns:
199 Status Set.
202 pg = context.scene.pdt_pg
203 obj = context.active_object
204 if all([bool(obj), obj.type == "MESH", obj.mode == "EDIT"]):
205 # must force edge selection mode here
206 bpy.context.tool_settings.mesh_select_mode = (False, True, False)
209 if obj.mode == "EDIT":
210 bm = bmesh.from_edit_mesh(obj.data)
212 selected_edges = [edge for edge in bm.edges if edge.select]
213 edge_indices = [i.index for i in selected_edges]
215 int_dict = get_intersection_dictionary(bm, edge_indices)
217 unselect_nonintersecting(bm, int_dict.keys(), edge_indices)
218 update_mesh(bm, int_dict)
220 bmesh.update_edit_mesh(obj.data)
221 else:
222 pg.error = f"{PDT_ERR_EDOB_MODE},{obj.mode})"
223 context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
224 return
226 return
227 else:
228 pg.error = f"{PDT_ERR_EDOB_MODE},{obj.mode})"
229 context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
230 return
232 class PDT_OT_IntersectAllEdges(bpy.types.Operator):
233 """Cut Selected Edges at All Intersections"""
235 bl_idname = "pdt.intersectall"
236 bl_label = "Intersect All Edges"
237 bl_options = {"REGISTER", "UNDO"}
239 @classmethod
240 def poll(cls, context):
241 """Check to see object is in correct condition.
243 Args:
244 context: Blender bpy.context instance.
246 Returns:
247 Boolean
249 obj = context.active_object
250 if obj is None:
251 return False
252 return obj is not None and obj.type == "MESH" and obj.mode == "EDIT"
254 def execute(self, context):
255 """Computes All intersections with Crossing Geometry.
257 Note:
258 Deletes original edges and replaces with new intersected edges
260 Args:
261 context: Blender bpy.context instance.
263 Returns:
264 Status Set.
267 pg = context.scene.pdt_pg
268 pg.command = f"intall"
269 return {"FINISHED"}